C---UE4-脚本编程秘籍-全-

C++ UE4 脚本编程秘籍(全)

原文:zh.annas-archive.org/md5/244B225FA5E3FFE01C9887B1851E5B64

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

虚幻引擎 4(UE4)是由游戏开发者制作的一套完整的游戏开发工具。本书提供 80 多个实用的配方,展示了在使用 UE4 开发游戏时如何利用 C++脚本的技术。我们将从在虚幻编辑器内添加和编辑 C++类开始。然后,我们将深入研究虚幻的主要优势之一 - 设计师可以定制程序员开发的角色和组件。这将帮助您了解何时以及如何使用 C++作为脚本工具的好处。本书将提供一系列以任务为导向的配方,为您提供有关使用 C++脚本游戏和使用 C++操纵游戏和开发环境的可行信息。在本书的最后,您将有能力成为使用 C++作为脚本语言的顶尖开发人员。

本书涵盖的内容

第一章,“UE4 开发工具”,概述了开始使用 UE4 游戏开发和用于创建游戏代码的基本工具的基本配方。

第二章,“创建类”,着重介绍如何创建与 UE4 蓝图编辑器良好集成的 C++类和结构。这些类将是称为 UCLASSES 的常规 C++类的毕业版本。

第三章,“内存管理和智能指针”,带领读者使用三种类型的指针,并提到了关于自动垃圾收集的一些常见陷阱。本章还向读者展示如何使用 Visual Studio 或 XCode 来解释崩溃或确认功能是否实现正确。

第四章,“角色和组件”,涉及创建自定义角色和组件,以及它们各自的作用以及它们如何协同工作。

第五章,“处理事件和委托”,描述了委托、事件和事件处理程序,并指导您通过创建它们自己的实现。

第六章,“输入和碰撞”,展示了如何将用户输入连接到 C++函数,以及如何从 UE4 中处理碰撞。它还将提供默认处理游戏事件,如用户输入和碰撞,允许设计师在必要时使用蓝图进行覆盖。

第七章,“类和接口之间的通信”,向您展示如何编写自己的 UInterfaces,并演示如何利用它们在 C++中最小化类耦合并帮助保持代码清晰。

第八章,“集成 C++和虚幻编辑器”,向您展示如何通过从头开始创建自定义蓝图和动画节点来自定义编辑器。我们还将实现自定义编辑器窗口和自定义详细面板,以检查用户创建的类型。

第九章,“用户界面 - UI 和 UMG”,演示了向玩家显示反馈是游戏设计中最重要的元素之一,这通常会涉及某种 HUD,或者至少是游戏中的菜单。

第十章,“控制 NPC 的人工智能”,涵盖了使用一点人工智能(AI)来控制 NPC 角色的食谱。

第十一章,“自定义材料和着色器”,讨论了在 UE4 编辑器中创建自定义材料和音频图节点。

第十二章,“使用 UE4 API”,解释了应用程序编程接口(API)是您作为程序员可以指示引擎(以及 PC)要做什么的方式。每个模块都有一个 API。要使用 API,有一个非常重要的链接步骤,您必须在ProjectName.Build.cs文件中列出您将在构建中使用的所有 API。

您需要为本书做什么

创建游戏是一项复杂的任务,需要资产和代码的结合。为了创建资产和代码,我们将需要一些非常先进的工具,包括美术工具,声音工具,级别编辑工具和代码编辑工具。资产包括任何视觉艺术品(2D 精灵,3D 模型),音频(音乐和音效)和游戏关卡。为此,我们将设置一个 C++编码环境来构建我们的 UE4 应用程序。我们将下载 Visual Studio 2015,安装它,并为 UE4 C++编码进行设置。(在编辑 UE4 游戏的 C++代码时,Visual Studio 是一个必不可少的代码编辑包。)

本书适合谁

本书适用于了解游戏设计和 C++基础知识,并希望将本机代码纳入 Unreal 制作的游戏中的游戏开发人员。他们将是希望扩展引擎或实现允许设计师在构建关卡时具有控制和灵活性的系统和角色的程序员。

部分

在本书中,您会经常看到几个标题(准备工作,如何做,工作原理,还有更多,另请参阅)。

为了清晰地说明如何完成食谱,我们使用以下部分:

准备工作

本节告诉您在食谱中可以期待什么,并描述了如何设置任何软件或食谱所需的任何初步设置。

如何做...

本节包含了遵循食谱所需的步骤。

工作原理...

本节通常包括对上一节中发生的事情的详细解释。

还有更多...

本节包含有关食谱的其他信息,以使读者对食谱更加了解。

另请参阅

本节提供了有关食谱的其他有用信息的有用链接。

约定

在本书中,您会发现一些区分不同类型信息的文本样式。以下是这些样式的一些示例及其含义的解释。

文本中的代码词,数据库表名,文件夹名,文件名,文件扩展名,路径名,虚拟 URL,用户输入和 Twitter 句柄显示如下:“传递给UPROPERTY()宏的参数指定了关于变量的一些重要信息。”

代码块设置如下:

#include<stdio.h>

int main()
{
  puts("Welcome to Visual Studio 2015 Community Edition!");
}

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

int intVar = 5;
float floatVar = 3.7f;
FString fstringVar = "an fstring variable";
UE_LOG(LogTemp, Warning, TEXT("Text, %d %f %s"), intVar, floatVar, *fstringVar );

新术语重要单词以粗体显示。您在屏幕上看到的单词,例如菜单或对话框中的单词,会以这样的方式出现在文本中:“在选择要添加到 Visual Studio 的工具后,单击下一步按钮。”

注意

警告或重要说明会出现在这样的框中。

提示

提示和技巧看起来像这样。

第一章:UE4 开发工具

在本章中,我们将概述 UE4 游戏开发的基本方法,并介绍我们用于创建使您的游戏的代码的基本工具。这将包括以下方法:

  • 安装 Visual Studio

  • 在 Visual Studio 中创建和构建您的第一个 C++项目

  • 在 Visual Studio 中更改代码字体和颜色

  • 扩展 - 在 Visual Studio 中更改颜色主题

  • 在 Visual Studio 中格式化您的代码(自动完成设置)

  • Visual Studio 中的快捷键

  • 在 Visual Studio 中扩展鼠标使用

  • UE4 - 安装

  • UE4 - 第一个项目

  • UE4 - 创建您的第一个级别

  • UE4 - 使用UE_LOG进行日志记录

  • UE4 - 从FStrings和其他变量创建FString

  • GitHub 上的项目管理 - 获取您的源代码控制

  • 在 GitHub 上的项目管理 - 使用问题跟踪器

  • 在 VisualStudio.com 上的项目管理 - 管理项目中的任务

  • 在 VisualStudio.com 上的项目管理 - 构建用户故事和任务

介绍

创建游戏是一个复杂的任务,需要结合资产代码。为了创建资产和代码,我们需要一些非常先进的工具,包括艺术工具声音工具级别编辑工具代码编辑工具。在本章中,我们将讨论寻找适合资产创建和编码的工具。资产包括任何视觉艺术品(2D 精灵、3D 模型)、音频(音乐和音效)和游戏级别。代码是指(通常是 C++)指示计算机如何将这些资产组合在一起以创建游戏世界和级别,并如何使该游戏世界“运行”的文本。每项任务都有数十种非常好的工具;我们将探索其中的一些,并提出一些建议。特别是游戏编辑工具是庞大的程序,需要强大的 CPU 和大量内存,以及非常好的 GPU 以获得良好的性能。

保护您的资产和工作也是必要的实践。我们将探讨和描述源代码控制,这是您如何在远程服务器上备份工作的方式。还包括Unreal Engine 4 编程的介绍,以及探索基本的日志记录功能和库的使用。还需要进行重要的规划来完成任务,因此我们将使用任务计划软件包来完成。

安装 Visual Studio

在编辑 UE4 游戏的 C++代码时,Visual Studio 是一个必不可少的代码编辑包。

准备工作

我们将建立一个 C++编码环境来构建我们的 UE4 应用程序。我们将下载 Visual Studio 2015,安装它,并为 UE4 C++编码进行设置。

如何做...

  1. 首先访问www.visualstudio.com/en-us/products/visual-studio-community-vs.aspx。单击下载 Community 2015。这将下载大约 200 KB 的加载程序/安装程序。如何做...

提示

您可以在www.visualstudio.com/en-us/products/compare-visual-studio-2015-products-vs.aspx上比较 Visual Studio 的版本。本书中的 UE4 开发目的,Visual Studio 的社区版是完全足够的。

  1. 启动安装程序,并选择要添加到您的 PC 的 Visual Studio 2015 组件。请记住,您选择的功能越多,安装的大小就越大。如何做...

上述屏幕截图显示了推荐的最小安装,所有都已选中Visual C++ 2015 的公共工具Git for WindowsVisual Studio 的 GitHub 扩展。我们将在本章的后面部分使用Git for Windows功能。

  1. 在您选择要添加到 Visual Studio 的工具后,单击下一步按钮。安装程序将下载所需的组件,并继续设置。安装时间取决于您的选项选择和连接速度,大约需要 20-40 分钟。

  2. 下载并安装 Visual Studio 2015 后,启动它。您将看到一个登录对话框。操作步骤...

您可以使用您的 Microsoft 帐户(用于登录 Windows 10 的帐户)登录,或者注册一个新帐户。登录或注册后,您将能够登录到 Visual Studio 本身。在登录到 Visual Studio 时,您可以选择(仅一次)Visualstudio.com 上托管的源代码库的唯一 URL。

工作原理...

Visual Studio 是一个优秀的编辑器,您将在其中编写代码时度过美好的时光。在下一个教程中,我们将讨论如何创建和编译您自己的代码。

在 Visual Studio 中创建和构建您的第一个 C++项目

为了从 Visual Studio 编译和运行代码,必须在项目内完成。

准备工作

在本教程中,我们将介绍如何从 Visual Studio 创建一个实际的可执行运行程序。我们将通过在 Visual Studio 中创建一个项目来实现这一点,以托管、组织和编译代码。

操作步骤...

在 Visual Studio 中,每组代码都包含在一个称为项目的东西中。项目是一组可构建的代码和资产,可以生成可执行文件(.exe可运行)或库(.lib.dll)。一组项目可以被收集到一起形成一个称为解决方案的东西。让我们首先为控制台应用程序构建一个 Visual Studio 解决方案和项目,然后构建一个 UE4 示例项目和解决方案。

  1. 打开 Visual Studio,转到文件 | 新建 | 项目...

  2. 您将看到以下对话框:操作步骤...

在左侧的窗格中选择Win32。在右侧的窗格中,点击Win32 控制台应用程序。在下方的框中命名您的项目,然后点击确定

  1. 在下一个对话框中,我们指定控制台应用程序的属性。阅读第一个对话框,然后简单地点击下一步。然后,在应用程序设置对话框中,选择控制台应用程序选项,然后在附加选项下选择空项目。您可以不选择安全开发生命周期(SDL)检查操作步骤...

  2. 应用程序向导完成后,您将创建您的第一个项目。将创建一个解决方案和一个项目。要查看这些内容,您需要解决方案资源管理器。为了确保解决方案资源管理器正在显示,转到视图 | 解决方案资源管理器(或按下Ctrl + Alt + L)。解决方案资源管理器通常显示在主编辑器窗口的左侧或右侧,如下面的屏幕截图所示:操作步骤...

解决方案资源管理器还显示了项目的所有文件。使用解决方案资源管理器,我们还将在编辑器中添加一个代码文件。右键单击您的项目FirstProject,然后选择添加 | 新建项...

操作步骤...

  1. 在下一个对话框中,只需选择C++文件 (.cpp),并给文件任何您喜欢的名称。我称我的为Main.cpp操作步骤...

  2. 一旦您添加了文件,它将出现在解决方案资源管理器中,位于您的FirstProject的源文件过滤器下。随着项目的增长,将会添加更多的文件到您的项目中。您可以使用以下文本编译和运行您的第一个 C++程序:

#include<stdio.h>

int main()
{
  puts("Welcome to Visual Studio 2015 Community Edition!");
}
  1. 按下Ctrl + Shift + B来构建项目,然后按下Ctrl + F5来运行项目。

  2. 您的可执行文件将被创建,您将看到一个小黑窗口显示程序运行的结果:操作步骤...

工作原理...

构建可执行文件涉及将您的 C++代码从文本语言转换为二进制文件。运行该文件将运行您的游戏程序,这只是发生在main()函数之间的代码文本,即在{}之间。

更多内容...

构建配置是我们应该在这里讨论的构建样式。至少有两个重要的构建配置需要了解:调试发布。所选的构建配置位于编辑器顶部,在默认位置的工具栏下方。

更多内容...

根据您选择的配置,将使用不同的编译器选项。调试配置通常在构建中包含大量的调试信息,并关闭优化以加快编译速度。发布构建通常经过优化(无论是为了大小还是速度),需要更长时间来构建,并且生成的可执行文件更小或更快。使用调试器进行逐步调试在调试模式下通常比发布模式更好。

在 Visual Studio 中更改代码字体和颜色

在 Visual Studio 中自定义字体和颜色不仅非常灵活,而且如果您的显示器分辨率非常高或非常低,您还会发现它非常必要。

准备工作

Visual Studio 是一个高度可定制的代码编辑工具。您可能会发现默认字体对于您的屏幕来说太小了。您可能想要更改代码的字体大小和颜色。或者您可能想要完全自定义关键字和文本背景颜色。字体和颜色对话框,我们将在本节中向您展示如何使用,允许您完全自定义代码编辑器字体和颜色的每个方面。

准备工作

如何做...

  1. 从 Visual Studio 中,转到工具 | 选项...如何做...

  2. 从出现的对话框中选择环境 | 字体和颜色。它将看起来像下面的截图:如何做...

  3. 尝试调整文本编辑器/纯文本的字体和字体大小。在对话框上点击确定,然后在代码文本编辑器中查看结果。如何做...

文本编辑器/纯文本描述了常规代码编辑器中所有代码文本使用的字体和大小。如果更改字体的大小,那么在编码窗口中输入的任何文本的大小都会改变(包括 C、C++、C#等所有语言)。

如何做...

每个项目的颜色(前景和背景)都可以完全自定义。尝试对文本编辑器/关键字设置(影响所有语言),或者对 C++特定项目进行设置,比如文本编辑器/C++函数。点击确定,您将看到项目的更改颜色在代码编辑器中得到反映。

您可能还想配置输出窗口的字体大小 - 选择显示设置 => 输出窗口,如下截图所示:

如何做...

输出窗口是编辑器底部显示构建结果和编译器错误的小窗口。

提示

无法保存(导出)或导入(导入)对字体和颜色对话框的更改。但是您可以使用一个叫做Visual Studio Theme Editor Extension的东西,了解更多请参考扩展 - 在 Visual Studio 中更改颜色主题来导出和导入自定义颜色主题。

因此,您可能希望避免从此对话框更改字体颜色。但是,您必须使用此对话框来更改字体和字体大小,无论在任何设置中(在撰写本文时)。

它是如何工作的...

字体和颜色对话框只是简单地改变了文本编辑器中代码的外观,以及输出窗口等其他窗口的外观。这对于使您的编码环境更加舒适非常有用。

更多内容...

一旦你自定义了你的设置,你会发现你可能想要保存你定制的字体和颜色设置供他人使用,或者放到另一台计算机上的另一个 Visual Studio 安装中。不幸的是,默认情况下,你无法保存你定制的字体和颜色设置。你需要一个叫做 Visual Studio Theme Editor 的扩展来做到这一点。我们将在下一个步骤中探讨这个问题。

另请参阅

  • 扩展 - 在 Visual Studio 中更改颜色主题部分描述了如何导入和导出颜色主题

扩展 - 在 Visual Studio 中更改颜色主题

默认情况下,你无法保存在字体和颜色对话框中所做的字体颜色和背景设置的更改。为了解决这个问题,Visual Studio 2015 有一个叫做主题的功能。如果你转到工具 | 选项 | 环境 | 常规,你可以将主题更改为三种预安装的主题之一(浅色蓝色深色)。

扩展 - 在 Visual Studio 中更改颜色主题

不同的主题会完全改变 Visual Studio 的外观-从标题栏的颜色到文本编辑器窗口的背景颜色。

你也可以完全自定义 Visual Studio 的主题,但你需要一个扩展来实现。扩展是可以安装到 Visual Studio 中以修改其行为的小程序。

默认情况下,你的定制颜色设置无法在没有扩展的情况下保存或重新加载到另一个 Visual Studio 安装中。有了扩展,你还可以保存自己的颜色主题以供他人使用。你还可以将另一个人或你自己制作的颜色设置加载到全新的 Visual Studio 副本中。

操作步骤...

  1. 转到工具 | 扩展和更新...

  2. 从出现的对话框中,在左侧面板中选择在线。在右侧的搜索框中开始输入Theme EditorVisual Studio 2015 Color Theme Editor对话框将会出现在你的搜索结果中。操作步骤...

  3. 点击条目右上角的小下载按钮。按照安装对话框提示进行操作,允许插件安装。安装完成后,Visual Studio 将提示你重新启动。

提示

或者,访问visualstudiogallery.msdn.microsoft.com/6f4b51b6-5c6b-4a81-9cb5-f2daa560430b并通过双击浏览器中的.vsix来下载/安装扩展。

  1. 点击立即重启以确保插件已加载。操作步骤...

  2. 重新启动后,转到工具 | 自定义颜色 打开颜色主题编辑页面。操作步骤...

  3. 从出现的颜色主题对话框中,点击你想要用作基础或起始主题的右上角小调色板形状图标(我在这里点击了浅色主题的调色板,如你在下面的截图中所见)。操作步骤...

  4. 颜色主题窗口的下部将出现一个自定义主题部分的主题副本。点击编辑主题来修改主题。当你编辑主题时,你可以改变从字体文本颜色到 C++关键字颜色的一切。

  5. 你感兴趣的主要区域是 C++文本编辑器部分。为了访问所有 C++文本编辑器选项,请确保在 Theme Editor 窗口顶部选择显示所有元素选项,如下截图所示:操作步骤...

注意

确保在 Theme Editor 窗口中选择显示所有元素选项,以显示特定于 C++的文本编辑器设置。否则,你只能进行 Chrome/GUI 类型的修改。

  1. 请注意,您感兴趣的大多数设置将在文本编辑器 | C/C++下,但有些设置不会有C++子标题。例如,编辑器窗口内的主/纯文本的设置(适用于所有语言)在文本编辑器 | 纯文本(没有C++子标题)下。

  2. 工具 | 选项 | 环境 | 常规中选择要使用的主题。您创建的任何新主题都将自动显示在下拉菜单中。

工作原理...

一旦加载插件,它会很好地集成到 Visual Studio 中。导出和上传您的主题以与他人共享也非常容易。

将主题添加到 Visual Studio 中,将其安装为工具 | 扩展和更新...中的扩展,要删除主题,只需卸载其扩展。

工作原理...

在 Visual Studio 中格式化您的代码(自动完成设置)

使用 Visual Studio 编写代码格式非常愉快。在本教程中,我们将讨论如何控制 Visual Studio 排列代码文本的方式。

准备工作

代码必须格式正确。如果代码一直保持一致的格式,您和您的合作程序员将能更好地理解、掌握并保持代码无错。这就是为什么 Visual Studio 在编辑器内包含许多自动格式化工具的原因。

如何做...

  1. 转到工具 | 选项 | 文本编辑器 | C/C++。此对话框显示一个窗口,允许您切换自动括号完成如何做...

自动括号完成是一种功能,当您键入{时,会自动为您键入相应的}。如果您不喜欢文本编辑器意外地插入字符,这个功能可能会让您不爽。

通常希望打开自动列出成员,因为这会显示一个漂亮的对话框,其中列出了您开始键入时的数据成员的完整名称。这样可以轻松记住变量名称,因此您不必记住它们:

如何做...

提示

如果您在代码编辑器中随时按Ctrl + Spacebar,将弹出自动列表。

  1. 更多的自动完成行为选项位于文本编辑器 | C/C++ | 格式下:如何做...

自动格式化部分:突出显示文本的部分,然后选择编辑 | 高级 | 格式化选择Ctrl + K, Ctrl + F)。

工作原理...

默认的自动完成和自动格式化行为可能会让您不爽。您需要与团队讨论如何格式化代码(空格或制表符缩进、缩进大小等),然后相应地配置您的 Visual Studio 设置。

Visual Studio 中的快捷键

编码时,快捷键确实可以节省您的时间。随时了解快捷键总是很好的。

准备工作

有许多快捷键可以让您的编码和项目导航更快速、更高效。在本教程中,我们将介绍如何使用一些常见的快捷键,以真正提高您的编码速度。

如何做...

以下是一些非常有用的键盘快捷键供您尝试:

  1. 单击代码的一页,然后单击其他地方,至少相隔 10 行代码。现在按下Ctrl + - [向后导航]。通过按Ctrl + -Ctrl + Shift + -分别可以导航到源代码的不同页面(您上次所在的位置和您现在所在的位置)。如何做...

提示

使用Ctrl + -在文本编辑器中跳转。光标将跳回到上次所在的位置,即使上次所在的位置距离代码超过 10 行,即使上次所在的位置在另一个文件中。

比如,例如,你正在一个地方编辑代码,然后你想回到你刚刚去过的地方(或者回到你来自的代码部分)。只需按下Ctrl + -,就会将你传送回到你上次所在的代码位置。要向前传送到你按下Ctrl + -之前所在的位置,按下Ctrl + Shift + -。要向后传送,前一个位置应该超过 10 行,或者在不同的文件中。这对应于工具栏中的前进和后退菜单按钮:

操作步骤...

提示

工具栏中的后退和前进导航按钮,分别对应Ctrl + -Ctrl + Shift + -的快捷键。

  1. 按下Ctrl + W可以高亮显示一个单词。

  2. 按住Ctrl + Shift + 右箭头(或左箭头)(不是Shift + 右箭头)来移动到光标的右侧和左侧,选择整个单词。

  3. 按下Ctrl + C复制文本,Ctrl + X剪切文本,Ctrl + V粘贴文本。

  4. 剪贴板环: 剪贴板环是对 Visual Studio 维护的最后一次复制操作堆栈的一种引用。通过按下Ctrl + C,你将正在复制的文本推送到一个有效的堆栈中。在不同的文本上再次按下Ctrl + C,将该文本推送到剪贴板堆栈中。例如,在下图中,我们先是在单词cyclic上按下了Ctrl + C,然后在单词paste上按下了Ctrl + C操作步骤...

如你所知,按下Ctrl + V会粘贴堆栈中的顶部项目。按下Ctrl + Shift + V会访问在该会话中曾经复制的所有项目的非常长的历史记录,也就是堆栈顶部项目下面的项目。在你用尽项目列表后,列表会回到堆栈顶部的项目。这是一个奇怪的功能,但你可能偶尔会发现它有用。

  1. Ctrl + MCtrl + M折叠代码部分。操作步骤...

操作原理...

键盘快捷键可以通过减少编码会话中必须执行的鼠标操作次数来加快代码编辑器中的工作速度。

在 Visual Studio 中扩展鼠标使用

鼠标是一个非常方便的选择文本的工具。在这一部分,我们将介绍如何以一种高级的方式使用鼠标快速编辑代码文本。

操作步骤...

  1. 按住Ctrl键单击以选择整个单词。操作步骤...

  2. 按住Alt键选择文本框(Alt + 左键单击 + 拖动)。操作步骤...

然后你可以剪切、复制或覆盖方框形的文本区域。

操作原理...

单纯的鼠标点击可能很繁琐,但通过Ctrl + Alt的帮助,它变得非常酷。尝试Alt + 左键单击 + 拖动来选择一行文本,然后进行输入。你输入的字符将在行中重复出现。

UE4 – 安装

安装和配置 UE4 需要遵循一系列步骤。在这个教程中,我们将详细介绍引擎的正确安装和设置。

准备工作

UE4 需要相当多的 GB 空间,所以你应该在目标驱动器上至少有 20GB 左右的空间来进行安装。

操作步骤...

  1. 访问 unrealengine.com 并下载它。如果需要,注册一个账户。

  2. 通过双击EpicGamesLauncherInstaller-x.x.x-xxx.msi安装程序来运行 Epic Games Launcher 程序的安装程序。在默认位置安装它。

  3. 安装 Epic Games Launcher 程序后,通过双击桌面上的图标或开始菜单中的图标打开它。

  4. 浏览起始页面,四处看看。最终,你需要安装一个引擎。点击UE4选项卡顶部左侧的大橙色安装引擎按钮,如下图所示:操作步骤...

  5. 弹出对话框将显示可以安装的组件。选择您想要安装的组件。建议首先安装前三个组件(核心组件入门内容模板和功能包)。如果不打算使用,可以不安装用于调试的编辑符号组件。如何操作...

  6. 引擎安装完成后,安装引擎按钮将变为启动引擎按钮。

它是如何工作的...

Epic Games Launcher 是您需要启动引擎本身的程序。它在选项卡中保存了所有您的项目和库的副本。

还有更多...

尝试在 | 保险库部分下载一些免费的库包。为此,请单击左侧的项目,并向下滚动,直到看到保险库,位于我的项目下方。

UE4 - 第一个项目

在 UE4 中设置项目需要多个步骤。重要的是要正确选择选项,以便您可以获得自己喜欢的设置,因此在构建第一个项目时,请仔细遵循这个配方。

在 UE4 中创建的每个项目至少占用 1GB 左右的空间,因此您应该决定是否要将创建的项目放在同一目标驱动器上,还是放在外部或单独的硬盘驱动器上。

如何操作...

  1. 从 Epic Games Launcher 中,单击启动虚幻引擎 4.11.2按钮。一旦您进入引擎,将出现创建新项目或加载现有项目的选项。

  2. 选择新项目选项卡。

  3. 决定您是否将使用 C++来编写项目,还是仅使用蓝图。

  4. 如果仅使用蓝图,从蓝图选项卡中选择要使用的模板。

  5. 如果除了蓝图之外还要使用 C++来构建项目,请从C++选项卡中选择项目模板来构建项目。

  6. 如果不确定要基于哪个模板编写代码,BASIC Code 是任何 C++项目的绝佳起点(或者对于仅蓝图的项目,选择 Blank)。

  7. 查看模板列表下方出现的三个图标。这里有三个配置选项:

  8. 您可以选择目标桌面或移动应用程序。

  9. 您可以选择修改质量设置(带有魔法植物的图片)。但您可能不需要修改这些。质量设置在引擎 | 引擎可扩展性设置下是可重新配置的。

  10. 最后一个选项是是否将入门内容包含在项目中。您可能可以在项目中使用入门内容包。它包含一些出色的材料和纹理。

提示

如果不喜欢入门内容包,请尝试 UE4 市场中的包。那里有一些出色的免费内容,包括GameTextures Material Pack

  1. 选择要保存项目的驱动器和文件夹。请记住,每个项目大约占用 1GB 的空间,您需要目标驱动器上至少有这么多的空间。

  2. 给您的项目命名。最好将其命名为与您计划创建的内容相关的独特名称。

  3. 点击创建。UE4 编辑器和 Visual Studio 2015 窗口都应该弹出,使您能够编辑您的项目。

提示

将来,请记住,您可以通过以下两种方法之一打开 Visual Studio 2015 Solution:

  • 通过您的本地文件浏览器。导航到项目存储的根目录,并双击ProjectName.sln文件。

  • 从 UE4 中,单击文件 | 打开 Visual Studio

UE4 - 创建您的第一个级别

在 UE4 中创建级别非常容易,并且通过一个很好的 UI 得到了很好的促进。在这个配方中,我们将概述基本的编辑器使用,并描述一旦您启动了第一个项目后如何构建您的第一个级别。

准备工作

完成上一个配方,UE4 - 第一个项目。一旦您构建了一个项目,我们就可以继续创建一个级别。

如何操作...

  1. 在开始新项目时设置的默认关卡将包含一些默认几何图形和风景。但是,您不需要从这些入门内容开始。如果您不想从中构建,可以删除它,或者创建一个新关卡。

  2. 要创建一个新关卡,请单击文件 | 新建关卡…,然后选择创建一个带有背景天空(默认)或不带背景天空(空关卡)的关卡。

提示

如果选择创建一个不带背景天空的关卡,请记住您必须向其添加灯光,以有效地查看您添加到其中的几何图形。

  1. 如果在项目创建时加载了入门内容(或其他内容),那么您可以使用内容浏览器将内容拉入您的关卡。只需从内容浏览器将您的内容实例拖放到关卡中,保存并启动它们。

  2. 使用模式面板(窗口 | 模式)向您的关卡添加一些几何图形。确保单击灯泡和立方体的图片以访问可放置的几何图形。您还可以通过单击模式选项卡上左侧的灯光子选项卡来添加灯光。如何做…

注意

模式面板包含两个有用的项目,用于构建关卡:一些示例几何图形(立方体和球等)以及一个充满灯光的面板。尝试这些并进行实验,开始布置您的关卡。

UE4 - 使用 UE_LOG 记录

记录对于输出内部游戏数据非常重要。使用日志工具可以让您将信息打印到 UE4 编辑器中一个方便的输出日志窗口中。

准备工作

在编码时,有时我们可能希望将一些调试信息发送到 UE 日志窗口。使用UE_LOG宏是可能的。日志消息是一种非常重要和方便的方式,可以在开发程序时跟踪信息。

如何做…

  1. 在您的代码中,输入一行代码,使用以下形式:
UE_LOG(LogTemp, Warning, TEXT("Some warning message") );

  1. 在 UE4 编辑器中打开输出日志,以便在程序运行时在该窗口中看到打印的日志消息。如何做…

它是如何工作的…

UE_LOG宏接受至少三个参数:

  • 日志类别(我们在这里使用LogTemp来表示临时日志中的日志消息)

  • 日志级别(我们在这里使用警告来表示以黄色警告文本打印的日志消息)

  • 用于日志消息文本的实际文本的字符串

不要忘记在日志消息文本周围使用TEXT()宏!它会将封闭的文本提升为 Unicode(它会在前面加上 L),当编译器设置为使用 Unicode 时。

UE_LOG也接受可变数量的参数,就像 C 编程语言中的printf()一样。

int intVar = 5;
float floatVar = 3.7f;
FString fstringVar = "an fstring variable";
UE_LOG(LogTemp, Warning, TEXT("Text, %d %f %s"), intVar, floatVar, *fstringVar );

在使用UE_LOG时,FString变量前面会有一个星号*,用于取消引用FString到常规的 C 样式TCHAR指针。

提示

TCHAR通常被定义为一个变量类型,如果编译中使用了 Unicode,则TCHAR解析为wchar_t。如果关闭了 Unicode(编译器开关_UNICODE未定义),那么TCHAR解析为简单的 char。

在不再需要来自源的日志消息时,不要忘记清除它们!

UE4 - 从 FStrings 和其他变量创建 FString

在 UE4 编码时,通常希望从变量构造一个字符串。使用FString::PrintfFString::Format函数非常容易。

准备工作

为此,您应该有一个现有的项目,可以在其中输入一些 UE4 C++代码。通过打印可以将变量放入字符串中。将变量打印到字符串中可能有些反直觉,但您不能简单地将变量连接在一起,希望它们会自动转换为字符串,就像 JavaScript 等某些语言中那样。

如何做…

  1. 使用FString::Printf()

  2. 考虑您想要打印到字符串中的变量。

  3. 打开并查看printf格式说明符的参考页面,例如en.cppreference.com/w/cpp/io/c/fprintf

  4. 尝试以下代码:

FString name = "Tim";
int32 mana = 450;
FString string = FString::Printf( TEXT( "Name = %s Mana = %d" ), *name, mana );

注意前面的代码块如何精确地使用格式说明符,就像传统的printf函数一样。在前面的示例中,我们使用%s将一个字符串放入格式化的字符串中,使用%d将一个整数放入格式化的字符串中。不同类型的变量存在不同的格式说明符,你应该在 cppreference.com 等网站上查找它们。

  1. 使用FString::Format()。以以下形式编写代码:
FString name = "Tim";
int32 mana = 450;
TArray< FStringFormatArg > args;
args.Add( FStringFormatArg( name ) );
args.Add( FStringFormatArg( mana ) );
FString string = FString::Format( TEXT( "Name = {0} Mana = {1}" ), args );
UE_LOG( LogTemp, Warning, TEXT( "Your string: %s" ), *string );

使用FString::Format(),而不是使用正确的格式说明符,我们使用简单的整数和FStringFormatArgTArrayFstringFormatArg帮助FString::Format()推断要放入字符串的变量类型。

GitHub 上的项目管理-获取你的源代码控制

在开发项目时非常重要的一件事是在工作时生成时间线历史。为此,你需要定期备份你的源代码。Git 是一个很好的工具,可以做到这一点。Git 允许你将更改(提交)存储到远程服务器上的在线存储库中,以便你的代码的开发历史被记录并保存在远程服务器上。如果你的本地副本出现了损坏,你总是可以从在线备份中恢复。你的代码库开发的时间线历史被称为源代码控制

准备工作

有一些免费的在线源备份服务。一些免费的存储数据的替代方案包括:

  • Visualstudio.com:有限/私人分享你的存储库

  • github.com:无限公开分享你的存储库

Visualstudio.com 非常适合免费为你的项目提供一些隐私,而 GitHub 非常适合免费与大量用户分享你的项目。Visualstudio.com 还提供一些非常好的工作板和规划功能,我们稍后会在本文中使用(GitHub 也提供竞争对手问题跟踪器,我们稍后也会讨论)。

你选择的网站主要取决于你计划如何分享你的代码。在本文中,我们将使用 GitHub 进行源代码存储,因为我们需要与大量用户(包括你!)分享我们的代码。

如何做...

  1. github.com注册一个 GitHub 账户。使用Team Explorer菜单(View | Team Explorer)登录到你的 GitHub 账户。

  2. 一旦打开Team Explorer,你可以使用Team Explorer窗口中出现的按钮登录到你的 GitHub 账户。

  3. 在你登录后,你应该获得CloneCreate存储库的能力。这些选项将出现在Team Explorer中 GitHub 菜单的正下方。

  4. 从这里,我们想要创建我们的第一个存储库。点击Create按钮,在弹出的窗口中命名你的存储库。

提示

在创建项目时,要小心从.gitignore选项菜单中选择VisualStudio选项。这将导致 Git 忽略你不想包含在存储库中的 Visual Studio 特定文件,例如构建和发布目录。

  1. 现在你有了一个存储库!存储库在 GitHub 上初始化。我们只需要把一些代码放进去。

  2. 打开 Epic Games Launcher,并创建一个要放入存储库的项目。

  3. 在 Visual Studio 2015 中打开 C++项目,右键单击解决方案。从出现的上下文菜单中选择Add Solution to Source Control。出现的对话框会询问你是否要使用Git还是TFVC

提示

如果你使用Git进行源代码控制,那么你可以托管在 github.com 或 Visualstudio.com 上。

  1. 在将 Git 源代码控制添加到项目后,再次查看Team Explorer。从那个窗口,你应该输入一个简短的消息,然后点击Commit按钮。

它是如何工作的...

Git 存储库对于备份代码和项目文件的副本在项目发展过程中非常重要。Git 中有许多命令可用于浏览项目历史记录(尝试 Git GUI 工具),查看自上次提交以来的更改(git diff),或在 Git 历史记录中向后和向前移动(git checkout commit-hash-id)。

GitHub 上的项目管理-使用问题跟踪器

跟踪您项目的进展、功能和错误非常重要。GitHub 问题跟踪器将使您能够做到这一点。

准备工作

跟踪您项目计划的功能和运行问题非常重要。GitHub 的问题跟踪器可用于创建您想要添加到项目中的功能列表,以及您需要在将来某个时候修复的错误。

如何做...

  1. 要向您的问题跟踪器添加问题,首先选择您想要编辑的存储库,方法是转到 GitHub 的首页并选择
  • 您输入错误或功能描述的框支持**Markdown****。Markdown 是一种简化的类似 HTML 的标记语言,让您可以轻松快速地编写类似 HTML 的语法。以下是一些 markdown 语法的示例:
# headings
## sub-headings
### sub-sub-headings
_italics_, __bold__, ___bold-italics___
[hyperlinks](http://towebsites.com/)

code (indented by 4 spaces), preceded by a blank line

* bulleted
* lists
  - sub bullets
    - sub sub bullets

>quotations

提示

如果您想了解更多关于 Markdown 语法的信息,请查看daringfireball.net/projects/markdown/syntax。** * 您还可以将问题标记为错误、增强(功能)或其他任何您喜欢的标签。通过问题** | 标签链接可以自定义标签:如何做...*** 从那里,您可以编辑、更改标签的颜色,或删除您的标签。我删除了所有的默认标签,并用feature替换了增强一词,如下两个屏幕截图所示:如何做...如何做...* 一旦您完全自定义了您的标签,您的 GitHub 问题跟踪器就会更容易导航。通过使用适当的标签对问题进行优先处理。 **## 它是如何工作的...

GitHub 的问题跟踪器是跟踪项目中的错误和功能的绝佳方式。使用它不仅可以组织您的工作流程,还可以保持项目上所做工作的出色历史记录。

另请参阅

  • 你还应该查看 Wiki 功能,它允许你记录你的源代码

在 VisualStudio.com 上的项目管理-管理项目中的任务

通常使用规划工具进行项目的高级管理。GitHub 的问题跟踪器可能满足您的需求,但如果您想要更多,Microsoft 的 Visual Studio Team Services 提供了ScrumKanban风格编程任务(功能,错误等)的规划工具。

使用此工具是组织任务的绝佳方式,以确保按时完成任务,并适应工业标准的工作流程。在安装过程中注册 Visual Studio 的社区版时,您的帐户将包括免费使用这些工具。

如何做...

在本节中,我们将描述如何使用 Visualstudio.com 上的Workboard功能来规划一些简单的任务。

  1. 要创建自己的项目 Workboard,请转到 Visualstudio.com 上的您的帐户。登录,然后选择概述选项卡。在最近的项目和团队标题下,选择新建链接。如何做...

  2. 向您的项目添加项目名称描述。在命名您的项目之后(我命名为Workboards),单击创建项目。您将等待一两秒钟以完成项目创建,然后在下一个对话框中单击导航到项目按钮。如何做...

  3. 下一个显示的屏幕允许您导航到Workboards区域。单击管理工作如何做...

  4. 管理工作屏幕是项目中要做的事情的看板式(即:优先级)任务队列。您可以点击新项目按钮来添加新项目到您的待办事项列表中。如何做…

提示

一旦您将某些东西添加到您的待办事项列表中,它就被称为是您的待办事项的一部分。在看板中,您总是落后!如果您是经理,您永远不希望待办事项为空。

工作原理…

您看板的待办事项中的每个项目都被称为用户故事。用户故事是敏捷软件开发术语,每个用户故事都应该描述特定最终用户的需求。例如,在前面的用户故事中,需求是有可视图形,用户故事描述了必须创建图形(精灵)来满足这个用户需求。

用户故事通常有一个特定的格式:

注意

作为<某人>,我想要<这样做>,这样我就可以<获得好处>。

例如:

注意

作为<游戏玩家>,我想要<重新组织物品>,这样我就可以<将热键设置为我想要的槽位>。

在工作板上,您将有许多用户故事。我之前放置了一些用户故事,所以我们可以与它们一起玩。

一旦您的看板上充满了用户故事,它们都将位于新的垂直列中。当您开始或者在特定用户故事上取得进展时,您可以将其从水平拖动到活跃,最后到已解决已关闭,当用户故事完成时。

工作原理…

在 VisualStudio.com 上进行项目管理 - 构建用户故事和任务

从 Scrum 的角度来看,用户故事是需要完成的任务的分组。一组用户故事可以被收集到一个特性中,一组特性可以被聚集到一个称为史诗的东西中。VisualStudio.com 非常好地组织了用户故事的创建,以便轻松构建和规划完成任何特定任务(用户故事)。在这个教程中,我们将描述如何组装和整理用户故事。

准备工作

在 VisualStudio.com 的项目管理套件中输入的每个项目都应该是某人希望出现在软件中的特性。用户故事的创建是一种有趣、简单和令人兴奋的方式,可以将一堆任务分组并分配给您的程序员作为要完成的工作。立即登录到您的 VisualStudio.com 帐户,编辑其中一个项目,并开始使用此功能。

如何做…

  1. 从 VisualStudio.com 的团队服务首页,导航到您想要输入一些新工作的项目。如果您点击最近的项目和团队下的浏览,您可以找到所有的项目。如何做…

  2. 选择您想要使用的项目并点击导航

  3. Visualstudio.com 中的任务发生在三种超级任务类别之一中:

  • 用户故事

  • 特性

  • 史诗

提示

用户故事,特性和史诗只是工作的组织单位。史诗包含许多特性。特性包含许多用户故事,用户故事包含许多任务。

默认情况下,史诗不会显示。您可以通过转到设置(屏幕右侧的齿轮图标)来显示史诗。然后导航到常规 | 待办事项。在仅查看您的团队管理的待办事项部分下,选择显示所有三种待办事项:史诗特性故事

  1. 在您可以将第一个任务(用户故事)输入到待办事项之前,现在有四个导航步骤要执行:

  2. 从顶部的菜单栏中选择工作

  3. 然后,在工作页面上出现的子菜单中,选择待办事项

  4. 在出现的侧边栏中,点击故事

  5. 从右侧面板中选择看板如何做…

提示

Backlog 是我们尚未完成的用户故事和任务集。你可能会想,“全新的任务真的会被输入到 Backlog 中吗?”没错!你已经落后了!Scrum 术语的含义似乎暗示着“工作过剩”。

  1. 从右侧面板中,点击新项目,并填写你的新用户故事项目的文本。

  2. 点击用户故事卡的文本,并填写受让人、它所属的迭代描述标签以及你想探索的详情选项卡的任何其他字段。

  3. 接下来,我们将整个用户故事分解为一系列可实现的任务。将鼠标悬停在新的用户故事项目上,直到出现省略号(三个点…)。点击省略号,然后选择+添加任务

  4. 列出完成用户故事的细节,以一系列任务的形式。

  5. 将每个任务分配给:

  • 一个受让人

  • 一个迭代

提示

简单来说,迭代实际上只是一个时间段。在每个迭代结束时,你应该有一个可交付的、可测试的软件完成品。迭代是一个时间段,指的是产生你惊人软件的另一个版本(用于测试和可能的发布)。

  1. 随着项目开发功能完成和错误修复,继续向项目添加任务。

它是如何工作的…

史诗包含许多特性。特性包含许多用户故事,用户故事包含许多任务和测试。

它是如何工作的…

所有这些项目都可以分配给一个用户(一个实际的人),以及一个迭代(时间段),用于分配责任和安排任务。一旦分配了这些,任务应该出现在查询选项卡中。

提示

在本书的前言中提到了下载代码包的详细步骤。请查看一下。

本书的代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Unreal-Engine-4-Scripting-with-CPlusPlus-Cookbook。我们还有来自丰富书籍和视频目录的其他代码包,可在github.com/PacktPublishing/上找到。去看看吧!

第二章:创建类

本章重点介绍如何创建与 UE4 蓝图编辑器良好集成的 C++类和结构。这些类是常规 C++类的毕业版本,称为UCLASS

提示

UCLASS只是一个带有大量 UE4 宏装饰的 C++类。这些宏生成额外的 C++头文件代码,使其能够与 UE4 编辑器本身集成。

使用UCLASS是一个很好的实践。如果配置正确,UCLASS宏可能会使你的UCLASS可蓝图化。使你的UCLASS可蓝图化的优势在于,它可以使你的自定义 C++对象具有蓝图可视编辑属性(UPROPERTY),并带有方便的 UI 小部件,如文本字段、滑块和模型选择框。你还可以在蓝图图表中调用函数(UFUNCTION)。这两者都显示在以下截图中:

创建类

在左边,两个装饰为UPROPERTY的类成员(一个UTexture引用和一个FColor)显示在 C++类的蓝图中进行编辑。在右边,一个标记为BlueprintCallable的 C++函数GetName显示为可以从蓝图图表中调用的UFUNCTION

注意

UCLASS宏生成的代码将位于ClassName.generated.h文件中,这将是你的UCLASS头文件ClassName.h中所需的最后一个#include

以下是本章将涵盖的主题:

  • 制作UCLASS-派生自UObject

  • 创建可编辑的UPROPERTY

  • 从蓝图中访问UPROPERTY

  • 指定UCLASS作为UPROPERTY的类型

  • 从你的自定义UCLASS创建蓝图

  • 实例化UObject派生类(ConstructObject <>NewObject <>

  • 销毁UObject派生类

  • 创建USTRUCT

  • 创建UENUM()

  • 创建UFUNCTION

提示

你会注意到,即使我们在这个类中创建的示例对象是可蓝图化的,它们也不会被放置在关卡中。这是因为为了放置在关卡中,你的 C++类必须派生自Actor基类,或者在其下。有关更多详细信息,请参见第四章,演员和组件

介绍

一旦你了解了模式,UE4 代码通常非常容易编写和管理。我们编写的代码用于从另一个UCLASS派生,或者创建UPROPERTYUFUNCTION非常一致。本章提供了围绕基本UCLASS派生、属性和引用声明、构造、销毁和一般功能的常见 UE4 编码任务的示例。

制作UCLASS-派生自 UObject

使用 C++编码时,你可以拥有自己的代码,编译并运行为本机 C++代码,适当调用newdelete来创建和销毁你的自定义对象。只要你的newdelete调用适当配对,以便在你的 C++代码中没有泄漏,本机 C++代码在你的 UE4 项目中是完全可接受的。

然而,你也可以声明自定义的 C++类,它们的行为类似于 UE4 类,通过将你的自定义 C++对象声明为UCLASSUCLASS使用 UE4 的智能指针和内存管理例程进行分配和释放,根据智能指针规则进行加载和读取,可以从蓝图中访问。

提示

请注意,当您使用UCLASS宏时,您的UCLASS对象的创建和销毁必须完全由 UE4 管理:您必须使用ConstructObject来创建对象的实例(而不是 C++本机关键字new),并调用UObject::ConditionalBeginDestroy()来销毁对象(而不是 C++本机关键字delete)。如何创建和销毁您的UObject派生类在本章后面的实例化 UObject 派生类(ConstructObject <>和 NewObject <>)销毁 UObject 派生类部分中有详细说明。

准备工作

在本配方中,我们将概述如何编写一个使用UCLASS宏的 C++类,以启用托管内存分配和释放,并允许从 UE4 编辑器和蓝图中访问。您需要一个 UE4 项目,可以在其中添加新代码以使用此配方。

如何做...

要创建自己的UObject派生类,请按照以下步骤进行:

  1. 从正在运行的项目中,在 UE4 编辑器中选择文件 | 添加 C++类

  2. 添加 C++类对话框中,转到窗口的右上方,选中显示所有类复选框:如何做...

  3. 通过选择从Object父类派生来创建UCLASSUObject是 UE4 层次结构的根。您必须选中此对话框右上角的显示所有类复选框,才能在列表视图中看到Object类。

  4. 选择Object(层次结构顶部)作为要继承的父类,然后单击下一步

提示

请注意,虽然对话框中将写入Object,但在您的 C++代码中,您将从实际上以大写U开头的UObject派生的 C++类。这是 UE4 的命名约定:

UObject(不在Actor分支上)派生的UCLASS必须以U开头命名。

Actor派生的UCLASS必须以A开头命名(第四章,“演员和组件”)。

不派生自UCLASS的 C++类(不具有命名约定),但可以以F开头命名(例如FAssetData)。

直接派生自UObject的派生类将无法放置在级别中,即使它们包含UStaticMeshes等可视表示元素。如果要将对象放置在 UE4 级别中,您至少必须从Actor类或其下的继承层次结构中派生。请参阅第四章,“演员和组件”了解如何从Actor类派生可放置在级别中的对象。

本章的示例代码将无法放置在级别中,但您可以在 UE4 编辑器中基于本章中编写的 C++类创建和使用蓝图。

  1. 为您的新的Object派生类命名,以适合您正在创建的对象类型。我称我的为UserProfile。在 UE4 生成的 C++文件中,这将显示为UUserObject,以确保遵循 UE4 的约定(C++ UCLASS前面加上U)。

  2. 转到 Visual Studio,并确保您的类文件具有以下形式:

#pragma once

#include "Object.h" // For deriving from UObject
#include "UserProfile.generated.h" // Generated code

// UCLASS macro options sets this C++ class to be 
// Blueprintable within the UE4 Editor
UCLASS( Blueprintable )
class CHAPTER2_API UUserProfile : public UObject
{
  GENERATED_BODY()
};
  1. 编译并运行您的项目。现在,您可以在 Visual Studio 和 UE4 编辑器中使用自定义的UCLASS对象。有关您可以使用它做什么的更多详细信息,请参阅以下配方。

工作原理…

UE4 为你的自定义UCLASS生成和管理大量的代码。这些代码是由 UE4 宏(如UPROPERTYUFUNCTIONUCLASS宏本身)的使用而生成的。生成的代码被放入UserProfile.generated.h中。你必须为了编译成功,将UCLASSNAME.generated.h文件与UCLASSNAME.h文件一起#include进来。如果不包含UCLASSNAME.generated.h文件,编译将失败。UCLASSNAME.generated.h文件必须作为UCLASSNAME.h#include列表中的最后一个#include包含进来:

正确 错误

|

#pragma once

#include "Object.h"
#include "Texture.h"
// CORRECT: .generated.h last file
#include "UserProfile.generated.h"

|

#pragma once

#include "Object.h"
#include "UserProfile.generated.h" 
// WRONG: NO INCLUDES AFTER
// .GENERATED.H FILE
#include "Texture.h"

|

UCLASSNAME.generated.h文件不是最后一个包含在包含列表中时,会出现错误:

>> #include found after .generated.h file - the .generated.h file should always be the last #include in a header

还有更多...

这里有一堆关键字,我们想在这里讨论,它们修改了UCLASS的行为方式。UCLASS可以标记如下:

  • Blueprintable:这意味着你希望能够在 UE4 编辑器内的Class Viewer中构建一个蓝图(右键单击时,创建蓝图类...变为可用)。如果没有Blueprintable关键字,即使你可以在Class Viewer中找到它并右键单击,创建蓝图类...选项也不会对你的UCLASS可用:还有更多...

  • 只有当你在UCLASS宏定义中指定了Blueprintable时,创建蓝图类...选项才可用。如果不指定Blueprintable,那么生成的UCLASS将不可用于蓝图。

  • BlueprintType:使用这个关键字意味着UCLASS可以作为另一个蓝图中的变量使用。你可以在任何蓝图的EventGraph的左侧面板的Variables组中创建蓝图变量。如果指定了NotBlueprintType,那么你不能将这个蓝图变量类型用作蓝图图表中的变量。在Class Viewer中右键单击UCLASS名称将不会显示创建蓝图类...还有更多...

任何指定了BlueprintTypeUCLASS都可以添加为蓝图类图表的变量列表。

你可能不确定是否将你的 C++类声明为UCLASS。这真的取决于你。如果你喜欢智能指针,你可能会发现UCLASS不仅可以使代码更安全,还可以使整个代码库更连贯和更一致。

另请参阅

  • 要向蓝图图表添加可编程的UPROPERTY,请参阅下面的创建可编辑的 UPROPERTY部分。有关使用适当的智能指针引用UCLASS实例的详细信息,请参阅第三章,内存管理和智能指针

创建可编辑的 UPROPERTY

你声明的每个UCLASS可以在其中声明任意数量的UPROPERTY。每个UPROPERTY可以是一个可视可编辑的字段,或者是UCLASS的蓝图可访问的数据成员。

我们可以为每个UPROPERTY添加一些限定符,这些限定符会改变它在 UE4 编辑器中的行为,比如EditAnywhere(可以更改UPROPERTY的屏幕)和BlueprintReadWrite(指定蓝图可以随时读写变量,而 C++代码也被允许这样做)。

准备工作

要使用这个方法,你应该有一个可以添加 C++代码的 C++项目。此外,你还应该完成前面的方法,制作一个 UCLASS - 派生自 UObject

如何做...

  1. 在你的UCLASS声明中添加成员如下:
UCLASS( Blueprintable )
class CHAPTER2_API UUserProfile : public UObject
{
  GENERATED_BODY()
  public:
  UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Stats)
  float Armor;
  UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Stats)
  float HpMax;
};
  1. 创建你的UObject类派生的蓝图,并通过从对象浏览器中双击打开 UE4 编辑器中的蓝图。

  2. 现在你可以在蓝图中为这些新的UPROPERTY字段的默认值指定值:如何做...

  3. 通过将蓝图类的几个实例拖放到您的级别中,并编辑放置的对象上的值(双击它们)来指定每个实例的值。

它是如何工作的...

传递给UPROPERTY()宏的参数指定了关于变量的一些重要信息。在前面的示例中,我们指定了以下内容:

  • EditAnywhere:这意味着UPROPERTY()宏可以直接从蓝图中编辑,或者在游戏级别中放置的每个UClass对象的每个实例上进行编辑。与以下进行对比:

  • EditDefaultsOnly:蓝图的值是可编辑的,但不能在每个实例上进行编辑

  • EditInstanceOnly:这将允许在UClass对象的游戏级实例中编辑UPROPERTY()宏,而不是在基蓝图本身上进行编辑

  • BlueprintReadWrite:这表示属性可以从蓝图图中读取和写入。带有BlueprintReadWriteUPROPERTY()必须是公共成员,否则编译将失败。与以下进行对比:

  • BlueprintReadOnly:属性必须从 C++中设置,不能从蓝图中更改

  • 类别:您应该始终为您的UPROPERTY()指定一个类别类别确定了UPROPERTY()将出现在属性编辑器中的哪个子菜单下。在类别=Stats下指定的所有UPROPERTY()将出现在蓝图编辑器中的相同Stats区域中。

另请参阅

从蓝图中访问 UPROPERTY

从蓝图中访问UPROPERTY非常简单。成员必须作为UPROPERTY公开在您的蓝图图中要访问的成员变量上。您必须在宏声明中限定UPROPERTY,指定它是BlueprintReadOnly还是BlueprintReadWrite,以指定您是否希望变量从蓝图中只读取(仅)或甚至可以从蓝图中写入。

您还可以使用特殊值BlueprintDefaultsOnly来指示您只希望默认值(在游戏开始之前)可以从蓝图编辑器中进行编辑。BlueprintDefaultsOnly表示数据成员不能在运行时从蓝图中编辑。

如何做到...

  1. 创建一些UObject派生类,指定BlueprintableBlueprintType,例如以下内容:
UCLASS( Blueprintable, BlueprintType )
class CHAPTER2_API UUserProfile : public UObject
{
  GENERATED_BODY()
  public:
  UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Stats)
  FString Name;
};

UCLASS宏中的BlueprintType声明是使用UCLASS作为蓝图图中的类型所必需的。

  1. 在 UE4 编辑器中,从 C++类派生一个蓝图类,如从自定义 UCLASS 创建蓝图中所示。

  2. 通过将实例从内容浏览器拖放到主游戏世界区域中,在 UE4 编辑器中创建您的蓝图派生类的实例。它应该出现为游戏世界中的一个圆形白色球,除非您已为其指定了模型网格。

  3. 在允许函数调用的蓝图图中(例如级别蓝图,通过蓝图 | 打开级别蓝图访问),尝试打印您的 Warrior 实例的Name属性,如下截图所示:如何做到...

提示

导航蓝图图很容易。右键单击并拖动以平移蓝图图;Alt +右键单击+拖动以缩放。

它是如何工作的...

UPROPERTY会自动为 UE4 类编写Get/Set方法。但是,它们不能在UCLASS中声明为private变量。如果它们没有声明为publicprotected成员,您将收到形式为的编译器错误:

>> BlueprintReadWrite should not be used on private members

指定 UCLASS 作为 UPROPERTY 的类型

因此,您已经构建了一些用于在 UE4 中使用的自定义UCLASS。但是如何实例化它们呢?UE4 中的对象是引用计数和内存管理的,因此您不应该直接使用 C++关键字new来分配它们。相反,您将不得不使用一个名为ConstructObject的函数来实例化您的UObject派生类。ConstructObject不仅需要您创建对象的 C++类,还需要一个 C++类的蓝图类派生(UClass*引用)。UClass*引用只是指向蓝图的指针。

我们如何在 C++代码中实例化特定蓝图的实例?C++代码不应该知道具体的UCLASS名称,因为这些名称是在 UE4 编辑器中创建和编辑的,您只能在编译后访问。我们需要以某种方式将蓝图类名称传递给 C++代码以实例化。

我们通过让 UE4 程序员从 UE4 编辑器中列出的所有可用蓝图(从特定 C++类派生)的简单下拉菜单中选择 C++代码要使用的UClass来实现这一点。为此,我们只需提供一个可编辑的UPROPERTY,其中包含一个TSubclassOf<C++ClassName>类型的变量。或者,您可以使用FStringClassReference来实现相同的目标。

这使得在 C++代码中选择UCLASS就像选择要使用的纹理一样。UCLASS应该被视为 C++代码的资源,它们的名称不应该硬编码到代码库中。

准备工作

在您的 UE4 代码中,您经常需要引用项目中的不同UCLASS。例如,假设您需要知道玩家对象的UCLASS,以便在代码中使用SpawnObject。从 C++代码中指定UCLASS非常麻烦,因为 C++代码根本不应该知道在蓝图编辑器中创建的派生UCLASS的具体实例。就像我们不希望将特定资产名称嵌入 C++代码中一样,我们也不希望将派生的蓝图类名称硬编码到 C++代码中。

因此,我们在 UE4 编辑器中使用 C++变量(例如UClassOfPlayer),并从蓝图对话框中进行选择。您可以使用TSubclassOf成员或FStringClassReference成员来实现,如下面的屏幕截图所示:

准备工作

如何做…

  1. 导航到您想要向其添加UCLASS引用成员的 C++类。例如,装饰一个类派生的UCLASS玩家相当容易。

  2. UCLASS内部,使用以下形式的代码声明UPROPERTY,允许选择从层次结构中派生的UObjectUClass(蓝图类):

UCLASS()
class CHAPTER2_API UUserProfile : public UObject
{
  UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Unit)
  TSubclassOf<UObject> UClassOfPlayer; // Displays any UClasses
  // deriving from UObject in a dropdown menu in Blueprints

  // Displays string names of UCLASSes that derive from
  // the GameMode C++ base class
  UPROPERTY( EditAnywhere, meta=(MetaClass="GameMode"), Category = Unit )
  FStringClassReference UClassGameMode;
};
  1. 将 C++类制作成蓝图,然后打开该蓝图。单击UClassOfPlayer菜单旁边的下拉菜单。

  2. 从列出的UClass的下拉菜单中选择适当的UClassOfPlayer成员。

它是如何工作的…

TSubclassOf

TSubclassOf< >成员将允许您在 UE4 编辑器内编辑具有TSubclassOf< >成员的任何蓝图时,使用下拉菜单指定UClass名称。

FStringClassReference

MetaClass标签是指您期望UClassName派生自哪个基本 C++类。这将限制下拉菜单的内容仅显示从该 C++类派生的蓝图。如果您希望显示项目中的所有蓝图,可以省略MetaClass标签。

从您的自定义 UCLASS 创建蓝图

制作蓝图只是从您的 C++对象派生蓝图类的过程。从您的 UE4 对象创建蓝图派生类允许您在编辑器中可视化编辑自定义UPROPERTY。这避免了将任何资源硬编码到您的 C++代码中。此外,为了使您的 C++类能够放置在关卡中,必须首先制作成蓝图。但是,只有在蓝图下面的 C++类是Actor类派生类时才可能。

注意

有一种方法可以使用FStringAssetReferencesStaticLoadObject来加载资源(例如纹理)。然而,通常不鼓励通过将路径字符串硬编码到您的 C++代码中来加载资源。在UPROPERTY()中提供可编辑的值,并从正确的具体类型的资产引用中加载是一个更好的做法。

准备工作

要按照此步骤进行操作,您需要有一个构建好的UCLASS,您希望从中派生一个蓝图类(请参阅本章前面的制作 UCLASS-从 UObject 派生部分)。您还必须在UCLASS宏中将您的UCLASS标记为Blueprintable,以便在引擎内部进行蓝图制作。

提示

任何在UCLASS宏声明中具有Blueprintable元关键字的UObject派生类都可以制作成蓝图。

如何操作…

  1. 要将您的UserProfile类制作成蓝图,首先确保UCLASSUCLASS宏中具有Blueprintable标记。应如下所示:
UCLASS( Blueprintable )
class CHAPTER2_API UUserProfile : public UObject
  1. 编译并运行您的代码。

  2. 类查看器中找到UserProfile C++类(窗口 | 开发人员工具 | 类查看器)。由于先前创建的UCLASS不是从Actor派生的,因此要找到您的自定义UCLASS,您必须在类查看器中关闭筛选器 | 仅显示角色(默认已选中):如何操作…

关闭仅显示角色复选标记,以显示类查看器中的所有类。如果不这样做,那么您的自定义 C++类可能不会显示!

提示

请记住,您可以使用类查看器中的小搜索框轻松找到UserProfile类,只需开始输入即可:

如何操作…

  1. 类查看器中找到您的UserProfile类,右键单击它,并通过选择创建蓝图…从中创建一个蓝图。

  2. 给您的蓝图命名。有些人喜欢在蓝图类名之前加上BP_。您可以选择遵循这个惯例,也可以不遵循,只要确保保持一致即可。

  3. 双击内容浏览器中出现的新蓝图,看一看。您将能够为创建的每个UserProfile蓝图实例编辑名称电子邮件字段。

它是如何工作的…

在 UE4 编辑器中,您创建的任何具有Blueprintable标记的 C++类都可以在蓝图中使用。蓝图允许您在 UE4 的可视 GUI 界面中自定义 C++类的属性。

实例化UObject派生类(ConstructObject <>和 NewObject <>)

在 C++中创建类实例通常使用关键字new。但是,UE4 实际上在内部创建其类的实例,并要求您调用特殊的工厂函数来生成任何要实例化的UCLASS的副本。您创建的是 UE4 蓝图类的实例,而不仅仅是 C++类。当您创建UObject派生类时,您将需要使用特殊的 UE4 引擎函数来实例化它们。

工厂方法允许 UE4 在对象上进行一些内存管理,控制对象在删除时的行为。该方法允许 UE4 跟踪对象的所有引用,以便在对象销毁时轻松取消所有对对象的引用。这确保了程序中不存在指向无效内存的悬空指针。

准备工作

实例化不是AActor类派生类的UObject派生类不使用UWorld::SpawnActor< >。相反,我们必须使用名为ConstructObject< >NewObject< >的特殊全局函数。请注意,您不应该使用裸的 C++关键字new来分配您的 UE4 UObject类派生的新实例。

您至少需要两个信息来正确实例化您的UCLASS实例:

  • 一个指向您想要实例化的类类型(蓝图类)的 C++类型的UClass引用。

  • 蓝图类派生的原始 C++基类

如何做...

  1. 在全局可访问的对象(如您的GameMode对象)中,添加一个TSubclassOf< YourC++ClassName > UPROPERTY()来指定并提供UCLASS名称给您的 C++代码。例如,我们在我们的GameMode对象中添加以下两行:
UPROPERTY( EditAnywhere, BlueprintReadWrite, Category = UClassNames )
TSubclassOf<UUserProfile> UPBlueprintClassName;
  1. 进入 UE4 编辑器,并从下拉菜单中选择您的UClass名称,以便您可以看到它的作用。保存并退出编辑器。

  2. 在您的 C++代码中,找到您想要实例化UCLASS实例的部分。

  3. 使用以下公式使用ConstructObject< >实例化对象:

ObjectType* object = ConstructObject< ObjectType >( UClassReference );

例如,使用我们在上一个示例中指定的UserProfile对象,我们将得到如下代码:

// Get the GameMode object, which has a reference to 
// the UClass name that we should instantiate:
AChapter2GameMode *gm = Cast<AChapter2GameMode>( GetWorld()->GetAuthGameMode() );
if( gm )
{
  UUserProfile* object = ConstructObject<UUserProfile>( gm->UPBlueprintClassName );
}

提示

如果您愿意,您也可以使用NewObject函数如下:

UProfile* object = NewObject<UProfile>( GetTransientPackage(), uclassReference );

它是如何工作的...

使用ConstructObjectNewObject实例化UObject类很简单。NewObjectConstructObject几乎做同样的事情:实例化一个蓝图类类型的对象,并返回正确类型的 C++指针。

不幸的是,NewObject有一个讨厌的第一个参数,它要求您在每次调用时传递GetTransientPackage()ConstructObject在每次调用时不需要此参数。此外,ConstructObject为您提供了更多的构造选项。

在构造您的 UE4 UObject派生类时不要使用关键字new!它将无法得到正确的内存管理。

还有更多...

NewObjectConstructObject是面向对象编程世界所谓的工厂。您要求工厂为您制造对象-您不会自己构造它。使用工厂模式使引擎能够轻松跟踪对象的创建过程。

销毁 UObject 派生类

在 UE4 中删除任何UObject派生类都很简单。当您准备删除您的UObject派生类时,我们只需在其上调用一个函数(ConditionalBeginDestroy())来开始拆卸。我们不使用本机 C++ delete命令来删除UObject派生类。我们将在下面的示例中展示这一点。

准备工作

您需要在任何未使用的UObject派生类上调用ConditionalBeginDestroy(),以便将其从内存中删除。不要调用delete来回收系统内存中的UObject派生类。您必须使用内部引擎提供的内存管理函数。下面将展示如何做到这一点。

如何做...

  1. 在您的对象实例上调用objectInstance->ConditionalBeginDestroy()

  2. 在您的客户端代码中将所有对objectInstance的引用设置为 null,并且在对其调用ConditionalBeginDestroy()之后不再使用objectInstance

它是如何工作的...

ConditionalBeginDestroy()函数通过删除所有内部引擎链接来开始销毁过程。这标记了引擎认为的对象销毁。然后,对象稍后通过销毁其内部属性,随后实际销毁对象来销毁。

在对象上调用了ConditionalBeginDestroy()之后,您(客户端)的代码必须考虑对象已被销毁,并且不能再使用它。

实际的内存恢复发生在ConditionalBeginDestroy()在对象上调用后的一段时间。有一个垃圾收集例程,它在固定时间间隔内完成清除游戏程序不再引用的对象的内存。垃圾收集器调用之间的时间间隔列在C:\Program Files (x86)\Epic Games\4.11\Engine\Config \BaseEngine.ini中,默认为每 60 秒进行一次收集:

gc.TimeBetweenPurgingPendingKillObjects=60

提示

如果在多次ConditionalBeginDestroy()调用后内存似乎不足,您可以通过调用GetWorld()->ForceGarbageCollection(true)来触发内存清理,以强制进行内部内存清理。

通常,除非您急需清除内存,否则无需担心垃圾收集或间隔。不要过于频繁地调用垃圾收集例程,因为这可能会导致游戏不必要的延迟。

创建一个 USTRUCT

您可能希望在 UE4 中构造一个蓝图可编辑的属性,其中包含多个成员。我们将在本章中创建的FColoredTexture结构将允许您将纹理和其颜色组合在同一结构中,以便在任何其他UObject衍生的Blueprintable类中进行包含和指定:

创建一个 USTRUCT

FColoredTexture结构确实在蓝图中具有上述图中显示的外观。

这是为了良好的组织和方便您的其他UCLASS``UPROPERTIES()。您可能希望在游戏中使用关键字struct构造一个 C++结构。

准备工作

UObject是所有 UE4 类对象的基类,而FStruct只是任何普通的 C++风格结构。所有使用引擎内的自动内存管理功能的对象必须从此类派生。

提示

如果您还记得 C++语言,C++类和 C++结构之间唯一的区别是 C++类具有默认的private成员,而结构默认为public成员。在 C#等语言中,情况并非如此。在 C#中,结构是值类型,而类是引用类型。

如何做...

我们将在 C++代码中创建一个名为FColoredTexture的结构,其中包含一个纹理和一个调制颜色:

  1. 在项目文件夹中创建一个名为ColoredTexture.h的文件(而不是FColoredTexture)。

  2. ColoredTexture.h包含以下代码:

#pragma once

#include "Chapter2.h"
#include "ColoredTexture.generated.h"

USTRUCT()
struct CHAPTER2_API FColoredTexture
{
  GENERATED_USTRUCT_BODY()
  public:
  UPROPERTY( EditAnywhere, BlueprintReadWrite, Category = HUD )
  UTexture* Texture;
  UPROPERTY( EditAnywhere, BlueprintReadWrite, Category = HUD )
  FLinearColor Color;
};
  1. 在一些可蓝图化的UCLASS()中,使用ColoredTexture.h作为UPROPERTY(),使用如下的UPROPERTY()声明:
UPROPERTY( EditAnywhere, BlueprintReadWrite, Category = HUD )
FColoredTexture* Texture;

它是如何工作的...

FColoredTexture指定的UPROPERTY()将显示为可编辑字段,当作为UPROPERTY()字段包含在另一个类中时,如步骤 3 所示。

还有更多...

将结构标记为USTRUCT()而不仅仅是普通的 C++结构的主要原因是与 UE4 引擎功能进行接口。您可以使用纯 C++代码(而不创建USTRUCT()对象)快速创建小型结构,而不要求引擎直接使用它们。

创建一个 UENUM()

C++的enum在典型的 C++代码中非常有用。UE4 有一种称为UENUM()的自定义枚举类型,它允许您创建一个将显示在正在编辑的蓝图内的下拉菜单中的enum

如何做...

  1. 转到将使用您指定的UENUM()的头文件,或创建一个名为EnumName.h的文件。

  2. 使用以下形式的代码:

UENUM()
enum Status
{
  Stopped     UMETA(DisplayName = "Stopped"),
  Moving      UMETA(DisplayName = "Moving"),
  Attacking   UMETA(DisplayName = "Attacking"),
};
  1. UCLASS()中使用您的UENUM()如下:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Status)
TEnumAsByte<Status> status;

它是如何工作的...

UENUM()在代码编辑器中显示为蓝图编辑器中的下拉菜单,您只能从中选择几个值。

创建一个 UFUNCTION

UFUNCTION()很有用,因为它们是可以从您的 C++客户端代码以及蓝图图表中调用的 C++函数。任何 C++函数都可以标记为UFUNCTION()

如何做...

  1. 构建一个UClass,其中包含您想要暴露给蓝图的成员函数。用UFUNCTION( BlueprintCallable, Category=SomeCategory)装饰该成员函数,以使其可以从蓝图中调用。例如,以下是再次提到的“战士”类:
// Warrior.h
class WRYV_API AWarrior : public AActor
{
  GENERATED_BODY()
  public:
  UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Properties)
  FString Name;
  UFUNCTION(BlueprintCallable, Category = Properties)
  FString ToString();
};

// Warrior.cpp
FString UProfile::ToString()
{
  return FString::Printf( "An instance of UProfile: %s", *Name );
}
  1. 通过将实例拖放到游戏世界上来创建您的“战士”类的实例。

  2. 从蓝图中,点击您的“战士”实例,调用ToString()函数。然后,在蓝图图表中,输入ToString()。它应该看起来像下面的截图:如何做...

提示

为了在实例上调用函数,在蓝图图表中开始输入自动完成菜单时,实例必须在世界大纲中被选中,如下面的截图所示:

如何做...

工作原理…

UFUNCTION()实际上是 C++函数,但具有额外的元数据,使它们可以被蓝图访问。

第三章:内存管理和智能指针

在本章中,我们将涵盖以下主题:

  • 未管理内存-使用malloc()/free()

  • 未管理内存-使用new/delete

  • 管理内存-使用NewObject< >ConstructObject< >

  • 管理内存-释放内存

  • 管理内存-智能指针(TSharedPtrTWeakPtrTAutoPtr)来跟踪对象

  • 使用TScopedPointer来跟踪对象

  • 虚幻引擎的垃圾收集系统和UPROPERTY()

  • 强制垃圾收集

  • 断点和逐步执行代码

  • 查找错误并使用调用堆栈

  • 使用分析器识别热点

介绍

内存管理始终是计算机程序中最重要的事情之一,以确保代码的稳定性和良好的无错误运行。悬空指针(指向已从内存中删除的内容的指针)是一个很难跟踪的错误示例。

介绍

在任何计算机程序中,内存管理都非常重要。UE4 的UObject引用计数系统是 Actor 和UObject衍生类的默认内存管理方式。这是在 UE4 程序中管理内存的默认方式。

如果您编写自己的自定义 C++类,这些类不是从UObject派生的,您可能会发现TSharedPtr/TWeakPtr引用计数类很有用。这些类为 0 引用对象提供引用计数和自动删除。

本章提供了 UE4 内存管理的示例。

未管理内存-使用 malloc()/free()

在 C(和 C++)中为计算机程序分配内存的基本方法是使用malloc()malloc()为程序的使用指定了计算机系统的内存块。一旦程序使用了一段内存,其他程序就无法使用或访问该段内存。尝试访问未分配给程序的内存段将生成“分段错误”,并在大多数系统上表示非法操作。

如何做...

让我们看一个示例代码,它分配了一个指针变量i,然后使用malloc()为其分配内存。我们在int后面的int*指针后面分配了一个整数。分配后,我们使用解引用运算符*int内存中存储一个值:

// CREATING AND ALLOCATING MEMORY FOR AN INT VARIABLE i
int* i; // Declare a pointer variable i
i = ( int* )malloc( sizeof( int ) ); // Allocates system memory
*i = 0; // Assign the value 0 into variable i
printf( "i contains %d", *i ); // Use the variable i, ensuring to 
// use dereferencing operator * during use
// RELEASING MEMORY OCCUPIED BY i TO THE SYSTEM
free( i ); // When we're done using i, we free the memory 
// allocated for it back to the system.
i = 0;// Set the pointer's reference to address 0

它是如何工作的...

前面的代码执行了后面图中所示的操作:

  1. 第一行创建了一个int*指针变量i,它起初是一个悬空指针,指向一个内存段,这个内存段可能对程序来说是无效的。

  2. 在第二个图中,我们使用malloc()调用来初始化变量i,使其指向一个大小恰好为int变量的内存段,这对于程序来说是有效的。

  3. 然后,我们使用命令*i = 0;初始化该内存段的内容为值0它是如何工作的...

提示

注意指针变量的赋值(i =)与赋值到指针变量引用的内存地址中的内容(*i =)之间的区别。

当变量i中的内存需要释放回系统时,我们使用free()释放调用,如下图所示。然后将i分配为指向内存地址0(由电气接地符号引用它是如何工作的...)。

它是如何工作的...

我们将变量i设置为指向NULL引用的原因是为了明确表明变量i不引用有效的内存段。

未管理内存-使用 new/delete

new运算符几乎与malloc调用相同,只是它在分配内存后立即调用对象的构造函数。使用new分配的对象应该使用delete运算符(而不是free())进行释放。

准备工作

在 C++中,使用malloc()被最佳实践替换为使用new运算符。malloc()new运算符功能的主要区别在于,new在内存分配后会调用对象类型的构造函数。

malloc new
为使用分配一块连续空间。 为使用分配一块连续空间。调用构造函数作为new运算符的参数使用的对象类型。

如何做...

在下面的代码中,我们声明了一个简单的Object类,然后使用new运算符构造了一个实例:

class Object
{
  Object()
  {
    puts( "Object constructed" );
  }
  ~Object()
  {
    puts( "Object destructed" );
  }
};
Object* object= new Object(); // Invokes ctor
delete object; // Invokes dtor
object = 0; // resets object to a null pointer

它是如何工作的...

new运算符的工作方式与malloc()一样,都是分配空间。如果与new运算符一起使用的类型是对象类型,则构造函数会自动使用关键字new调用,而使用malloc()则永远不会调用构造函数。

还有更多...

应该避免使用关键字new(或malloc)进行裸堆分配。引擎内部首选使用托管内存,以便跟踪和清理所有内存使用。如果分配了UObject派生类,绝对需要使用NewObject< >ConstructObject< >(在后续的示例中有详细介绍)。

托管内存-使用 NewObject< >和 ConstructObject< >

托管内存是指由 C++中的newdeletemallocfree调用之上的某个编程子系统分配和释放的内存。通常创建这些子系统是为了程序员在分配内存后不会忘记释放内存。未释放的、占用但未使用的内存块称为内存泄漏。例如:

for( int i = 0; i < 100; i++ )
int** leak = new int[500]; // generates memory leaks galore!

在上面的例子中,分配的内存没有被任何变量引用!因此,您既不能在for循环之后使用分配的内存,也不能释放它。如果您的程序分配了所有可用的系统内存,那么会发生的是您的系统将完全耗尽内存,您的操作系统将标记您的程序并关闭它,因为它使用了太多内存。

内存管理可以防止忘记释放内存。在内存管理程序中,通常由动态分配的对象记住引用该对象的指针数量。当引用该对象的指针数量为零时,它要么立即被自动删除,要么在下一次运行垃圾回收器时被标记为删除。

在 UE4 中,使用托管内存是自动的。必须使用NewObject< >()SpawnActor< >()来分配引擎内部使用的对象。释放对象是通过删除对对象的引用,然后偶尔调用垃圾清理例程(在本章后面列出)来完成的。

准备工作

当您需要构造任何不是Actor类的UObject派生类时,您应该始终使用NewObject< >。只有当对象是Actor或其派生类时,才应该使用SpawnActor< >

如何做...

假设我们要构造一个类型为UAction的对象,它本身是从UObject派生的。例如,以下类:

UCLASS(BlueprintType, Blueprintable, meta=(ShortTooltip="Base class for any Action type") )
Class WRYV_API UAction : public UObject
{
  GENERATED_UCLASS_BODY()
  public:
  UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Properties)
  FString Text;
  UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Properties)
  FKey ShortcutKey;
};

要构造UAction类的实例,我们可以这样做:

UAction* action = NewObject<UAction>( GetTransientPackage(),
UAction::StaticClass() /* RF_* flags */ );

它是如何工作的...

在这里,UAction::StaticClass()可以获取UAction对象的基本UClass*NewObject< >的第一个参数是GetTransientPackage(),它只是为游戏检索瞬态包。在 UE4 中,包(UPackage)只是一个数据集合。在这里,我们使用瞬态包来存储我们的堆分配数据。您还可以使用蓝图中的UPROPERTY() TSubclassOf<AActor>来选择UClass实例。

第三个参数(可选)是一组参数的组合,指示内存管理系统如何处理UObject

还有更多...

还有一个与NewObject<>非常相似的函数叫做ConstructObject<>ConstructObject<>在构造时提供了更多的参数,如果您需要指定这些参数,您可能会发现它很有用。否则,NewObject也可以正常工作。

另请参阅

托管内存-释放内存

当没有对UObject实例的引用时,UObject是引用计数和垃圾回收的。使用ConstructObject<>NewObject<>UObject类派生类上分配的内存也可以通过调用UObject::ConditionalBeginDestroy()成员函数手动释放(在引用计数降至 0 之前)。

准备工作

只有在您确定不再需要UObjectUObject类派生实例时才会这样做。使用ConditionalBeginDestroy()函数释放内存。

如何做…

以下代码演示了UObject 类的释放:

UObject *o = NewObject< UObject >( ... );
o->ConditionalBeginDestroy();

它是如何工作的…

命令ConditionalBeginDestroy()开始了释放过程,调用了BeginDestroy()FinishDestroy()可重写函数。

还有更多…

注意不要在其他对象的指针仍在内存中引用的对象上调用UObject::ConditionalBeginDestroy()

托管内存-智能指针(TSharedPtr、TWeakPtr、TAutoPtr)跟踪对象

当人们担心会忘记为他们创建的标准 C++对象调用delete时,他们经常使用智能指针来防止内存泄漏。TSharedPtr是一个非常有用的 C++类,它将使任何自定义 C++对象引用计数——除了UObject派生类,它们已经是引用计数的。还提供了一个名为TWeakPtr的替代类,用于指向引用计数对象,具有无法阻止删除的奇怪属性(因此称为“弱”)。

托管内存-智能指针(TSharedPtr、TWeakPtr、TAutoPtr)跟踪对象

提示

UObject及其派生类(使用NewObjectConstructObject创建的任何内容)不能使用TSharedPtr

准备工作

如果您不想在不使用UObject派生类的 C++代码中使用原始指针并手动跟踪删除,那么该代码是使用智能指针(如TSharedPtrTSharedRef等)的良好选择。当您使用动态分配的对象(使用关键字new创建)时,您可以将其包装在一个引用计数指针中,以便自动发生释放。不同类型的智能指针确定智能指针的行为和删除调用时间。它们如下:

  • TSharedPtr:线程安全(如果您将ESPMode::ThreadSafe作为模板的第二个参数)的引用计数指针类型,表示一个共享对象。当没有对它的更多引用时,共享对象将被释放。

  • TAutoPtr:非线程安全的共享指针。

如何做…

我们可以使用一个简短的代码段来演示先前提到的四种智能指针的使用。在所有这些代码中,起始指针可以是原始指针,也可以是另一个智能指针的副本。您只需将 C++原始指针包装在TSharedPtrTSharedRefTWeakPtrTAutoPtr的任何构造函数调用中。

例如:

// C++ Class NOT deriving from UObject
class MyClass { };
TSharedPtr<MyClass>sharedPtr( new MyClass() );

它是如何工作的…

弱指针和共享指针之间存在一些差异。弱指针在引用计数降至 0 时无法保持对象在内存中。

使用弱指针(而不是原始指针)的优势在于,当弱指针下面的对象被手动删除(使用ConditionalBeginDestroy()),弱指针的引用将变为NULL引用。这使你可以通过检查形式为的语句来检查指针下面的资源是否仍然正确分配:

if( ptr.IsValid() ) // Check to see if the pointer is valid
{
}

还有更多…

共享指针是线程安全的。这意味着底层对象可以在单独的线程上安全地进行操作。请记住,你不能在UObjectUObject派生类上使用TSharedRef,只能在自定义的 C++类上使用TSharedPtrTSharedRefTWeakPtr类,或者在你的FStructures上使用任何TSharedPtrTSharedRefTWeakPtr类来封装原始指针。你必须使用TWeakObjectPointerUPROPERTY()作为指向对象的智能指针的起点。

如果不需要TSharedPtr的线程安全保证,可以使用TAutoPtr。当对该对象的引用数量降至 0 时,TAutoPtr将自动删除该对象。

使用 TScopedPointer 跟踪对象

作用域指针是在声明它的块结束时自动删除的指针。请记住,作用域只是变量“存活”的代码段。作用域将持续到第一个出现的闭括号}

例如,在以下代码块中,我们有两个作用域。外部作用域声明一个整数变量x(在整个外部块中有效),而内部作用域声明一个整数变量y(在声明它的行之后的内部块中有效):

{
  int x;
  {
    int y;
  } // scope of y ends
} // scope of x ends

准备工作

当重要的引用计数对象(可能会超出范围)需要在使用期间保留时,作用域指针非常有用。

如何做…

要声明一个作用域指针,我们只需使用以下语法:

TScopedPointer<AWarrior> warrior(this );

这声明了一个指向在尖括号内声明的类型对象的作用域指针:<AWarrior>

它是如何工作的…

TScopedPointer变量类型会自动为指向的变量添加引用计数。这可以防止在作用域指针的生命周期内至少释放底层对象。

Unreal 的垃圾回收系统和 UPROPERTY()

当你有一个对象(比如TArray< >)作为UCLASS()UPROPERTY()成员时,你需要将该成员声明为UPROPERTY()(即使你不会在蓝图中编辑它),否则TArray将无法正确分配内存。

如何做…

假设我们有以下的UCLASS()宏:

UCLASS()
class MYPROJECT_API AWarrior : public AActor
{
  //TArray< FSoundEffect > Greets; // Incorrect
  UPROPERTY() TArray< FSoundEffect > Greets; // Correct
};

你必须将TArray成员列为UPROPERTY(),以便它能够正确地进行引用计数。如果不这样做,你将在代码中遇到意外的内存错误类型 bug。

它是如何工作的…

UPROPERTY()声明告诉 UE4,TArray必须得到适当的内存管理。没有UPROPERTY()声明,你的TArray将无法正常工作。

强制进行垃圾回收

当内存填满时,你想要释放一些内存时,可以强制进行垃圾回收。你很少需要这样做,但在有一个非常大的纹理(或一组纹理)需要清除的情况下,你可以这样做。

准备工作

只需在所有想要从内存中释放的UObject上调用ConditionalBeginDestroy(),或将它们的引用计数设置为 0。

如何做…

通过调用以下方式执行垃圾回收:

GetWorld()->ForceGarbageCollection( true );

断点和逐步执行代码

断点是用来暂停 C++程序,暂时停止代码运行,并有机会分析和检查程序操作的方式。你可以查看变量,逐步执行代码,并更改变量值。

准备工作

在 Visual Studio 中设置断点很容易。你只需在想要暂停操作的代码行上按下F9,或者单击代码行左侧的灰色边距。当操作到达指定行时,代码将暂停。

如何做…

  1. 按下F9,在您希望执行暂停的行上添加断点。这将在代码中添加一个断点,如下面的屏幕截图所示,用红点表示。单击红点可切换它。如何做…

  2. 生成配置设置为标题中带有调试的任何配置(DebugGame编辑器或者如果您将在没有编辑器的情况下启动,则简单地选择DebugGame)。

  3. 通过按下F5(不按住Ctrl),或选择调试 | 开始调试菜单选项来启动您的代码。

  4. 当代码到达红点时,代码的执行将暂停。

  5. 暂停的视图将带您进入调试模式的代码编辑器。在此模式下,窗口可能会重新排列,解决方案资源管理器可能会移动到右侧,并且新窗口会出现在底部,包括本地变量监视 1调用堆栈。如果这些窗口没有出现,请在调试 | 窗口子菜单下找到它们。

  6. 本地变量窗口(调试 | 窗口 | 本地变量)下检查您的变量。

  7. 按下F10跨过一行代码。

  8. 按下F11以进入一行代码。

工作原理…

调试器是强大的工具,允许您在代码运行时查看关于代码的一切,包括变量状态。

在代码行上方跨过一行(F10)会执行整行代码,然后立即在下一行再次暂停程序。如果代码行是一个函数调用,那么函数将在不暂停在函数调用的第一行的情况下执行。例如:

void f()
{
  // F11 pauses here
  UE_LOG( LogTemp, Warning, TEXT( "Log message" ) );
}
int main()
{
  f(); // Breakpoint here: F10 runs and skips to next line
}

进入一行代码(F11)将在接下来要运行的代码的下一行暂停执行。

查找错误并使用调用堆栈

当您的代码中有错误时,Visual Studio 会停止并允许您检查代码。Visual Studio 停止的位置不一定总是错误的确切位置,但可能会接近。它至少会在不能正确执行的代码行处。

准备就绪

在这个示例中,我们将描述调用堆栈,以及如何追踪您认为错误可能来自的位置。尝试向您的代码中添加一个错误,或者在您想要暂停进行检查的有趣位置添加一个断点。

如何做…

  1. 通过按下F5或选择调试 | 开始调试菜单选项,运行代码直到出现错误的地方。例如,添加以下代码行:
UObject *o = 0; // Initialize to an illegal null pointer
o->GetName(); // Try and get the name of the object (has bug)
  1. 代码将在第二行(o->GetName())暂停。

  2. 当代码暂停时,转到调用堆栈窗口(调试 | 窗口 | 调用堆栈)。

工作原理…

调用堆栈是已执行的函数调用列表。发生错误时,发生错误的行将列在调用堆栈的顶部。

工作原理…

使用性能分析器识别热点

C++性能分析器非常有用,可以找到需要大量处理时间的代码部分。使用性能分析器可以帮助您找到在优化期间需要关注的代码部分。如果您怀疑某个代码区域运行缓慢,那么如果在性能分析器中没有突出显示,您实际上可以确认它不会运行缓慢。

如何做…

  1. 转到调试 | 启动诊断工具(无调试)…如何做…

  2. 在前面的屏幕截图中显示的对话框中,选择您希望显示的分析类型。您可以选择分析CPU 使用情况GPU 使用情况内存使用情况,或者通过性能向导逐步选择您想要看到的内容。

  3. 单击对话框底部的开始按钮。

  4. 在短时间内(不到一两分钟)停止代码以停止采样。

提示

不要收集太多样本,否则性能分析器将需要很长时间才能启动。

  1. 检查出现在.diagsession文件中的结果。一定要浏览所有可用的选项卡。可用的选项卡将根据执行的分析类型而变化。

工作原理…

C++性能分析器对运行的代码进行采样和分析,并向您呈现一系列关于代码执行情况的图表和数据。

第四章:Actor 和组件

在本章中,我们将涵盖以下示例:

  • 在 C++中创建自定义Actor

  • 使用SpawnActor实例化Actor

  • 使用Destroy和定时器销毁Actor

  • 使用SetLifeSpan在延迟后销毁Actor

  • 通过组合实现Actor功能

  • 使用FObjectFinder将资产加载到组件中

  • 通过继承实现Actor功能

  • 附加组件以创建层次结构

  • 创建自定义Actor组件

  • 创建自定义Scene组件

  • 创建自定义Primitive组件

  • 为 RPG 创建InventoryComponent

  • 创建OrbitingMovement组件

  • 创建生成单位的建筑物

介绍

Actor 是在游戏世界中具有一定存在的类。Actor 通过合并组件获得其专门功能。本章涉及创建自定义 Actor 和组件,它们的作用以及它们如何一起工作。

在 C++中创建自定义 Actor

在 Unreal 默认安装的一些不同类型的 Actor 中,您可能会发现自己在项目开发过程中需要创建自定义的 Actor。这可能发生在您需要向现有类添加功能时,将组件组合成默认子类中不存在的组合,或者向类添加额外的成员变量时。接下来的两个示例演示了如何使用组合或继承来自定义 Actor。

准备工作

确保您已经按照第一章中的示例安装了 Visual Studio 和 Unreal 4,UE4 开发工具。您还需要有一个现有项目,或者使用 Unreal 提供的向导创建一个新项目。

如何做...

  1. 在 Unreal Editor 中打开您的项目,然后单击Content Browser中的Add New按钮:如何做...

  2. 选择New C++ Class...如何做...

  3. 在打开的对话框中,从列表中选择Actor如何做...

  4. 给您的 Actor 一个名称,比如MyFirstActor,然后单击OK启动 Visual Studio。

提示

按照惯例,Actor子类的类名以A开头。在使用此类创建向导时,请确保不要为您的类添加A前缀,因为引擎会自动为您添加前缀。

如何做...

  1. 当 Visual Studio 加载时,您应该看到与以下列表非常相似的内容:
MyFirstActor.h
#pragma once

#include "GameFramework/Actor.h"
#include "MyFirstActor.generated.h"

UCLASS()
class UE4COOKBOOK_API AMyFirstActor : public AActor
{
  GENERATED_BODY()
  public:
  AMyFirstActor(); 
};
MyFirstActor.cpp
#include "UE4Cookbook.h"
#include "MyFirstActor.h"
AMyFirstActor::AMyFirstActor()
{
  PrimaryActorTick.bCanEverTick = true;
}

它是如何工作的...

随着时间的推移,您将熟悉标准代码,因此您将能够在不使用 Unreal 向导的情况下直接从 Visual Studio 创建新类。

  • #pragma once: 这个预处理器语句,或者pragma,是 Unreal 预期的实现包含保护的方法——防止多次引用include文件导致错误。

  • #include "GameFramework/Actor.h": 我们将创建一个Actor子类,因此自然需要包含我们从中继承的类的header文件。

  • #include "MyFirstActor.generated.h": 所有 actor 类都需要包含它们的generated.h文件。这个文件是根据它在您的文件中检测到的宏自动由Unreal Header Tool (UHT)创建的。

  • UCLASS(): UCLASS是这样一个宏,它允许我们指示一个类将暴露给 Unreal 的反射系统。反射允许我们在运行时检查和迭代对象属性,以及管理对我们对象的引用以进行垃圾回收。

  • class UE4COOKBOOK_API AMyFirstActor : public AActor:这是我们类的实际声明。UE4COOKBOOK_API宏是由 UHT 创建的,通过确保项目模块的类在 DLL 中正确导出,可以帮助我们的项目在 Windows 上正确编译。你还会注意到MyFirstActorActor都有前缀A——这是虚幻要求的从Actor继承的本地类的命名约定。

  • GENERATED_BODY(): GENERATED_BODY是另一个 UHT 宏,已经扩展到包括底层 UE 类型系统所需的自动生成函数。

  • PrimaryActorTick.bCanEverTick = true;:在构造函数实现中,这一行启用了这个Actor的 tick。所有的 Actor 都有一个名为Tick的函数,这个布尔变量意味着Actor将每帧调用一次该函数,使得Actor能够在每帧执行必要的操作。作为性能优化,默认情况下是禁用的。

使用 SpawnActor 实例化一个 Actor

对于这个配方,你需要准备一个Actor子类来实例化。你可以使用内置类,比如StaticMeshActor,但最好练习使用上一个配方中创建的自定义Actor

如何操作...

  1. 创建一个新的 C++类,就像在上一个配方中一样。这次,选择GameMode作为基类,给它起一个名字,比如UE4CookbookGameMode

  2. 在你的新GameMode类中声明一个函数重写:

virtual void BeginPlay() override;
  1. .cpp文件中实现BeginPlay
void AUE4CookbookGameMode::BeginPlay()
{
  Super::BeginPlay();
  GEngine->AddOnScreenDebugMessage(-1, -1, FColor::Red, TEXT("Actor Spawning"));

  FTransform SpawnLocation;
  GetWorld()->SpawnActor<AMyFirstActor>( AMyFirstActor::StaticClass(), &SpawnLocation);
}
  1. 编译你的代码,可以通过 Visual Studio 或者在虚幻编辑器中点击编译按钮来进行。如何操作...

  2. 通过点击设置工具栏图标,然后从下拉菜单中选择World Settings,打开当前级别的World Settings面板。在GameMode Override部分,将游戏模式更改为你刚刚创建的GameMode子类,如下两个截图所示:如何操作...如何操作...

  3. 启动关卡,并通过查看World Outliner面板来验证GameMode是否在世界中生成了你的Actor的副本。你可以通过查看屏幕上显示的Actor Spawning文本来验证BeginPlay函数是否正在运行。如果没有生成,请确保世界原点没有障碍物阻止Actor生成。你可以通过在World Outliner面板顶部的搜索栏中输入来搜索世界中的对象列表,以过滤显示的实体。如何操作...

工作原理...

  1. GameMode是一种特殊类型的 Actor,它是虚幻游戏框架的一部分。地图的GameMode在游戏启动时由引擎自动实例化。

  2. 通过将一些代码放入自定义GameModeBeginPlay方法中,我们可以在游戏开始时自动运行它。

  3. BeginPlay中,我们创建一个FTransform,用于SpawnActor函数。默认情况下,FTransform被构造为零旋转,并且位置在原点。

  4. 然后,我们使用GetWorld获取当前级别的UWorld实例,然后调用它的SpawnActor函数。我们传入之前创建的FTransform,以指定对象应该在其位置即原点处创建。

使用 Destroy 和定时器销毁一个 Actor

这个配方将重用上一个配方中的GameMode,所以你应该先完成它。

如何操作...

  1. GameMode声明进行以下更改:
UPROPERTY()
AMyFirstActor* SpawnedActor;
UFUNCTION()
void DestroyActorFunction();
  1. 在实现文件的包含中添加#include "MyFirstActor.h"

  2. SpawnActor的结果分配给新的SpawnedActor变量:

SpawnedActor = GetWorld()->SpawnActor<AMyFirstActor> (AMyFirstActor::StaticClass(), SpawnLocation);
  1. BeginPlay函数的末尾添加以下内容:
FTimerHandle Timer;
GetWorldTimerManager().SetTimer(Timer, this, &AUE4CookbookGameMode::DestroyActorFunction, 10);
  1. 最后,实现DestroyActorFunction
void AUE4CookbookGameMode::DestroyActorFunction()
{
  if (SpawnedActor != nullptr)
  {
    SpawnedActor->Destroy();
  }
}
  1. 加载你在上一个配方中创建的具有自定义类游戏模式的关卡。

  2. 播放你的关卡,并使用 Outliner 验证你的SpawnedActor在 10 秒后被删除。

它的工作原理...

  • 我们声明一个UPROPERTY来存储我们生成的Actor实例,并创建一个自定义函数来调用,以便我们可以在计时器上调用Destroy()
UPROPERTY()
AMyFirstActor* SpawnedActor;
UFUNCTION()
void DestroyActorFunction();
  • BeginPlay中,我们将生成的Actor分配给我们的新UPROPERTY
SpawnedActor = GetWorld()->SpawnActor<AMyFirstActor> (AMyFirstActor::StaticClass(), SpawnLocation);
  • 然后我们声明一个TimerHandle对象,并将其传递给GetWorldTimerManager::SetTimerSetTimer在 10 秒后调用DestroyActorFunction指向的对象。SetTimer返回一个对象,一个句柄,允许我们在必要时取消计时器。SetTimer函数将TimerHandle对象作为引用参数传入,因此我们提前声明它,以便正确地将其传递给函数:
FTimerHandle Timer;
GetWorldTimerManager().SetTimer(Timer, this, &AUE4CookbookGameMode::DestroyActorFunction, 10);
  • DestroyActorFunction检查我们是否有一个有效的生成Actor的引用:
void AUE4CookbookGameMode::DestroyActorFunction()
{
  if (SpawnedActor != nullptr)
}
  • 如果这样做,它会调用实例上的Destroy,因此它将被销毁,并最终被垃圾回收:
SpawnedActor->Destroy();

使用 SetLifeSpan 延迟销毁 Actor

让我们看看如何销毁一个Actor

如何做...

  1. 使用向导创建一个新的 C++类。选择Actor作为你的基类。

  2. Actor的实现中,将以下代码添加到BeginPlay函数中:

SetLifeSpan(10);
  1. 将你的自定义Actor的一个副本拖到编辑器中的视口中。

  2. 播放你的关卡,并查看 Outliner,以验证你的Actor实例在 10 秒后消失,自行销毁。

它的工作原理...

  1. 我们将代码插入到BeginPlay函数中,以便在游戏启动时执行。

  2. SetLifeSpan(10);SetLifeSpan函数允许我们指定持续时间(以秒为单位),之后Actor调用自己的Destroy()方法。

通过组合实现 Actor 功能

没有组件的自定义 Actor 没有位置,也不能附加到其他 Actor。没有根组件,Actor 没有基本变换,因此它没有位置。因此,大多数 Actor 至少需要一个组件才能有用。

我们可以通过组合创建自定义 Actor-向我们的Actor添加多个组件,其中每个组件提供所需的一些功能。

准备工作

这个示例将使用在 C++中创建自定义 Actor中创建的Actor类。

如何做...

  1. 通过在public部分进行以下更改,在你的自定义类中添加一个新成员:
UPROPERTY()
UStaticMeshComponent* Mesh;
  1. 在 cpp 文件的构造函数中添加以下行:
Mesh = CreateDefaultSubobject<UStaticMeshComponent>("BaseMeshComponent");
  1. 验证你的代码看起来像以下片段,并通过编辑器中的Compile按钮编译它,或者在 Visual Studio 中构建项目:
UCLASS()
class UE4COOKBOOK_API AMyFirstActor : public AActor
{
  GENERATED_BODY()
  public:
  AMyFirstActor();

  UPROPERTY() 
  UStaticMeshComponent* Mesh;
};

#include "UE4Cookbook.h"
#include "MyFirstActor.h"
AMyFirstActor::AMyFirstActor()
{
  PrimaryActorTick.bCanEverTick = true;

  Mesh = CreateDefaultSubobject<UStaticMeshComponent>("BaseMeshComponent");
}
  1. 编译此代码后,将类的一个实例从Content Browser拖到游戏环境中,您将能够验证它现在具有变换和其他属性,例如来自我们添加的StaticMeshComponent的 Static Mesh。

它的工作原理...

  1. 我们在类声明中添加的UPROPERTY 宏是一个指针,用于保存我们作为Actor子对象的组件。
UPROPERTY()
UStaticMeshComponent* Mesh;
  1. 使用UPROPERTY()宏确保指针中声明的对象被视为引用,并且不会被垃圾回收(即删除),从而使指针悬空。

  2. 我们使用了一个 Static Mesh 组件,但任何Actor组件子类都可以工作。请注意,星号与变量类型连接在一起,符合 Epic 的样式指南。

  3. 在构造函数中,我们使用template函数将指针初始化为已知的有效值,template<class TReturnType> TReturnType* CreateDefaultSubobject(FName SubobjectName, bool bTransient = false)

  4. 这个函数负责调用引擎代码来适当初始化组件,并返回一个指向新构造对象的指针,以便我们可以给我们的组件指针一个默认值。这很重要,显然,以确保指针始终具有有效值,最大程度地减少对未初始化内存的引用风险。

  5. 该函数是基于要创建的对象类型进行模板化的,但还接受两个参数——第一个是子对象的名称,理想情况下应该是可读的,第二个是对象是否应该是瞬态的(即不保存在父对象中)。

另请参阅

  • 以下食谱向您展示如何在静态网格组件中引用网格资产,以便可以在不需要用户在编辑器中指定网格的情况下显示它

使用 FObjectFinder 将资产加载到组件中

在上一个食谱中,我们创建了一个静态网格组件,但我们没有尝试加载一个网格来显示组件。虽然在编辑器中可以做到这一点,但有时在 C++中指定默认值会更有帮助。

准备工作

按照上一个食谱,这样您就有了一个准备好的自定义Actor子类,其中包含一个静态网格组件。

在您的内容浏览器中,单击查看选项按钮,然后选择显示引擎内容

准备工作

浏览到引擎内容,然后到基本形状,看看我们将在这个食谱中使用的立方体

准备工作

如何做...

  1. 将以下代码添加到您的类的构造函数中:
auto MeshAsset = ConstructorHelpers::FObjectFinder<UStaticMesh>(TEXT("StaticMesh'/Engine/BasicShapes/Cube.Cube'"));
if (MeshAsset.Object != nullptr)
{
  Mesh->SetStaticMesh(MeshAsset.Object);
}
  1. 编译,并在编辑器中验证您的类的实例现在具有网格作为其视觉表示。

工作原理...

  • 我们创建了FObjectFinder类的一个实例,将要加载的资产类型作为模板参数传递进去。

  • FObjectFinder是一个类模板,帮助我们加载资产。当我们构造它时,我们传入一个包含我们要加载的资产路径的字符串。

  • 字符串的格式为"{ObjectType}'/Path/To/Asset.Asset'"。请注意字符串中使用了单引号。

  • 为了获取已经存在于编辑器中的资产的字符串,您可以在内容浏览器中右键单击资产,然后选择复制引用。这会给您一个字符串,这样您就可以将其粘贴到您的代码中。工作原理...

  • 我们使用了 C++11 中的auto关键字,以避免在声明中输入整个对象类型;编译器会为我们推断出类型。如果没有auto,我们将不得不使用以下代码:

ConstructorHelpers::FObjectFinder<UStaticMesh> MeshAsset = ConstructorHelpers::FObjectFinder<UStaticMesh>(TEXT("StaticMesh'/Engine/BasicShapes/Cube.Cube'"));
  • FObjectFinder类有一个名为Object的属性,它要么有指向所需资产的指针,要么是NULL,如果找不到资产。

  • 这意味着我们可以将其与nullptr进行比较,如果它不是空的,就使用SetStaticMesh将其分配给Mesh

通过继承实现 Actor 功能

继承是实现自定义Actor的第二种方法。这通常是为了创建一个新的子类,它添加成员变量、函数或组件到现有的Actor类中。在这个食谱中,我们将向自定义的GameState子类添加一个变量。

如何做...

  1. 在虚幻编辑器中,单击内容浏览器中的添加新内容,然后单击新建 C++类...,然后选择GameState作为基类,然后给您的新类起一个名字。

  2. 将以下代码添加到新类头文件中:

AMyGameState(); 

UFUNCTION()
void SetScore(int32 NewScore);

UFUNCTION()
int32 GetScore();
private:
UPROPERTY()
int32 CurrentScore;
  1. 将以下代码添加到 cpp 文件中:
AMyGameState::AMyGameState()
{
  CurrentScore = 0;
}

int32 AMyGameState::GetScore()
{
  return CurrentScore;
}

void AMyGameState::SetScore(int32 NewScore)
{
  CurrentScore = NewScore;
}
  1. 确认您的代码看起来像以下清单,并使用虚幻编辑器中的编译按钮进行编译:
MyGameState.h
#pragma once

#include "GameFramework/GameState.h"
#include "MyGameState.generated.h"

/**
*
*/
UCLASS()
class UE4COOKBOOK_API AMyGameState : public AGameState
{
  GENERATED_BODY()
  public:
  AMyGameState();

  UPROPERTY()
  int32 CurrentScore;

  UFUNCTION()
  int32 GetScore();

  UFUNCTION()
  void SetScore(uint32 NewScore);
};
MyGameState.cpp
#include "UE4Cookbook.h"
#include "MyGameState.h"

AMyGameState::AMyGameState()
{
  CurrentScore = 0;
}

int32 AMyGameState::GetScore()
{
  return CurrentScore;
}

void AMyGameState::SetScore(uint32 NewScore)
{
  CurrentScore = NewScore;
}

工作原理...

  1. 首先,我们添加了默认构造函数的声明:
AMyGameState();
  1. 这使我们能够在对象初始化时将我们的新成员变量设置为安全的默认值0
AMyGameState::AMyGameState()
{
  CurrentScore = 0;
}
  1. 在声明新变量时,我们使用int32类型,以确保在虚幻引擎支持的各种编译器之间具有可移植性。这个变量将负责在游戏运行时存储当前游戏分数。与往常一样,我们将使用UPROPERTY标记我们的变量,以便它能够得到适当的垃圾回收。这个变量被标记为private,所以改变值的唯一方式是通过我们的函数:
UPROPERTY()
int32 CurrentScore;
  1. GetScore函数将检索当前分数,并将其返回给调用者。它被实现为一个简单的访问器,只是返回基础成员变量。

  2. 第二个函数SetScore设置成员变量的值,允许外部对象请求更改分数。将此请求作为函数确保GameState可以审核此类请求,并且仅在有效时才允许它们,以防止作弊。此类检查的具体内容超出了本配方的范围,但SetScore函数是进行此类检查的适当位置。

  3. 我们的分数函数使用UFUNCTION宏声明有多种原因。首先,UFUNCTION可以通过一些额外的代码被蓝图调用或重写。其次,UFUNCTION可以标记为exec—这意味着它们可以在游戏会话期间由玩家或开发人员作为控制台命令运行,这样可以进行调试。

另请参阅

  • 第八章, 集成 C++和虚幻编辑器, 有一个名为创建新控制台命令的配方,您可以参考有关exec和控制台命令功能的更多信息

将组件附加到创建层次结构

在从组件创建自定义 Actor 时,考虑“附加”的概念非常重要。将组件附加在一起会创建一个关系,其中应用于父组件的变换也会影响附加到它的组件。

如何做...

  1. 使用编辑器基于Actor创建一个新类,并将其命名为HierarchyActor

  2. 将以下属性添加到您的新类中:

UPROPERTY()
USceneComponent* Root;
UPROPERTY()
USceneComponent* ChildSceneComponent;
UPROPERTY()
UStaticMeshComponent* BoxOne;
UPROPERTY()
UStaticMeshComponent* BoxTwo;
  1. 将以下代码添加到类构造函数中:
Root = CreateDefaultSubobject<USceneComponent>("Root");
ChildSceneComponent = CreateDefaultSubobject<USceneComponent>("ChildSceneComponent");
BoxOne = CreateDefaultSubobject<UStaticMeshComponent>("BoxOne");
BoxTwo = CreateDefaultSubobject<UStaticMeshComponent>("BoxTwo");

auto MeshAsset = ConstructorHelpers::FObjectFinder<UStaticMesh>(TEXT("StaticMesh'/Engine/BasicShapes/Cube.Cube'"));
if (MeshAsset.Object != nullptr)
{
  BoxOne->SetStaticMesh(MeshAsset.Object);
  BoxTwo->SetStaticMesh(MeshAsset.Object);
}
RootComponent = Root;
BoxOne->AttachTo(Root);
BoxTwo->AttachTo(ChildSceneComponent);
ChildSceneComponent->AttachTo(Root);
ChildSceneComponent->SetRelativeTransform(FTransform(FRotator(0, 0, 0), FVector(250, 0, 0), FVector(0.1f)));
  1. 验证您的代码是否如下所示:
HierarchyActor.h
#pragma once

#include "GameFramework/Actor.h"
#include "HierarchyActor.generated.h"

UCLASS()
class UE4COOKBOOK_API AHierarchyActor : public AActor
{
  GENERATED_BODY()
  public:
  AHierarchyActor();
  virtual void BeginPlay() override;
  virtual void Tick( float DeltaSeconds ) override;
  UPROPERTY()
  USceneComponent* Root;
  UPROPERTY()
  USceneComponent* ChildSceneComponent;
  UPROPERTY()
  UStaticMeshComponent* BoxOne;
  UPROPERTY()
  UStaticMeshComponent* BoxTwo;
};
HierarchyActor.cpp

#include "UE4Cookbook.h"
#include "HierarchyActor.h"

AHierarchyActor::AHierarchyActor()
{
  PrimaryActorTick.bCanEverTick = true;
  Root = CreateDefaultSubobject<USceneComponent>("Root");
  ChildSceneComponent = CreateDefaultSubobject<USceneComponent>("ChildSceneComponent");
  BoxOne = CreateDefaultSubobject<UStaticMeshComponent>("BoxOne");
  BoxTwo = CreateDefaultSubobject<UStaticMeshComponent>("BoxTwo");
  auto MeshAsset = ConstructorHelpers::FObjectFinder<UStaticMesh>(TEXT("StaticMesh'/Engine/BasicShapes/Cube.Cube'"));
  if (MeshAsset.Object != nullptr)
  {
    BoxOne->SetStaticMesh(MeshAsset.Object);
    BoxOne->SetCollisionProfileName(UCollisionProfile::Pawn_ProfileName);
    BoxTwo->SetStaticMesh(MeshAsset.Object);
    BoxTwo->SetCollisionProfileName(UCollisionProfile::Pawn_ProfileName);	
  }
  RootComponent = Root;
  BoxOne->AttachTo(Root);
  BoxTwo->AttachTo(ChildSceneComponent);
  ChildSceneComponent->AttachTo(Root);
  ChildSceneComponent->SetRelativeTransform(FTransform(FRotator(0, 0, 0), FVector(250, 0, 0), FVector(0.1f)));
}
void AHierarchyActor::BeginPlay()
{
  Super::BeginPlay();
}
void AHierarchyActor::Tick( float DeltaTime )
{
  Super::Tick( DeltaTime );
}
  1. 编译并启动编辑器。将 HierarchyActor 的副本拖入场景中。!如何做...

  2. 验证Actor在层次结构中是否有组件,并且第二个框的大小较小。!如何做...

它是如何工作的...

  1. 像往常一样,我们为我们的 Actor 创建一些带有UPROPERTY标记的组件。我们创建了两个场景组件和两个静态网格组件。

  2. 在构造函数中,我们像往常一样为每个组件创建默认子对象。

  3. 然后,我们加载静态网格,如果加载成功,将其分配给两个静态网格组件,以便它们具有视觉表示。

  4. 然后,我们通过附加组件在我们的Actor中构建了一个层次结构。

  5. 我们将第一个场景组件设置为Actor根。此组件将确定应用于层次结构中所有其他组件的变换。

  6. 然后,我们将第一个框附加到我们的新根组件,并将第二个场景组件作为第一个组件的父级。

  7. 我们将第二个框附加到我们的子场景组件,以演示更改该场景组件上的变换如何影响其子组件,但不影响对象中的其他组件。

  8. 最后,我们设置场景组件的相对变换,使其从原点移动一定距离,并且是比例的十分之一。

  9. 这意味着在编辑器中,您可以看到BoxTwo组件继承了其父组件ChildSceneComponent的平移和缩放。

创建自定义 Actor 组件

Actor 组件是实现应该在 Actor 之间共享的常见功能的简单方法。Actor 组件不会被渲染,但仍然可以执行操作,比如订阅事件或与包含它们的 Actor 的其他组件进行通信。

如何做...

  1. 使用编辑器向导创建一个名为RandomMovementComponentActorComponent。将以下类说明符添加到UCLASS宏中:
UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
  1. 在类头文件中添加以下UPROPERTY
UPROPERTY()
float MovementRadius;
  1. 将以下内容添加到构造函数的实现中:
MovementRadius = 0;
  1. 最后,将以下内容添加到TickComponent()的实现中:
AActor* Parent = GetOwner();
if (Parent)
{
  Parent->SetActorLocation(
  Parent->GetActorLocation() +
  FVector(
  FMath::FRandRange(-1, 1)* MovementRadius,
  FMath::FRandRange(-1, 1)* MovementRadius,
  FMath::FRandRange(-1, 1)* MovementRadius));
}
  1. 验证您的代码是否如下所示:
#pragma once
#include "Components/ActorComponent.h"
#include "RandomMovementComponent.generated.h"
UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class UE4COOKBOOK_API URandomMovementComponent : public UActorComponent
{
  GENERATED_BODY()
  public:
  URandomMovementComponent();
  virtual void BeginPlay() override;
  virtual void TickComponent( float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction ) override;
  UPROPERTY()
  float MovementRadius;
};

#include "UE4Cookbook.h"
#include "RandomMovementComponent.h"
URandomMovementComponent::URandomMovementComponent()
{
  bWantsBeginPlay = true;
  PrimaryComponentTick.bCanEverTick = true;
  MovementRadius = 5;
}

void URandomMovementComponent::BeginPlay()
{
  Super::BeginPlay();
}

void URandomMovementComponent::TickComponent( float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction )
{
  Super::TickComponent( DeltaTime, TickType, ThisTickFunction );
  AActor* Parent = GetOwner();
  if (Parent)
  {
    Parent->SetActorLocation(
    Parent->GetActorLocation() +
    FVector(
    FMath::FRandRange(-1, 1)* MovementRadius,
    FMath::FRandRange(-1, 1)* MovementRadius,
    FMath::FRandRange(-1, 1)* MovementRadius));
  }
}
  1. 编译您的项目。在编辑器中,创建一个空的Actor,并将Random Movement组件添加到其中。要做到这一点,从放置选项卡中将空 Actor拖到级别中,然后在详细信息面板中单击添加组件,并选择Random Movement。再次执行相同的操作以添加Cube组件,以便您有东西来可视化 actor 的位置。如何做...如何做...

  2. 播放你的关卡,并观察 actor 在每次调用TickComponent函数时随机移动的位置改变。

它是如何工作的...

  1. 首先,在组件声明中使用的UCLASS宏中添加一些说明符。将BlueprintSpawnableComponent添加到类的元值中意味着可以在编辑器中将组件的实例添加到蓝图类中。ClassGroup说明符允许我们指示组件在类列表中属于哪个类别:
UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
  1. MovementRadius作为新组件的属性添加,允许我们指定组件在单个帧中允许漫游的距离:
UPROPERTY()
float MovementRadius;
  1. 在构造函数中,我们将此属性初始化为安全的默认值:
MovementRadius =5;
  1. TickComponent是引擎每帧调用的函数,就像Tick对于 Actors 一样。在其实现中,我们检索组件所有者的当前位置,即包含我们组件的Actor,并在世界空间中生成一个偏移量:
AActor* Parent = GetOwner();
if (Parent)
{
  Parent->SetActorLocation(
  Parent->GetActorLocation() +
  FVector(
  FMath::FRandRange(-1, 1)* MovementRadius,
  FMath::FRandRange(-1, 1)* MovementRadius,
  FMath::FRandRange(-1, 1)* MovementRadius)
  );
}
  1. 我们将随机偏移添加到当前位置以确定新位置,并将拥有的 actor 移动到该位置。这会导致 actor 的位置在每一帧随机改变并且跳动。

创建自定义 Scene Component

Scene组件是Actor组件的子类,具有变换,即相对位置、旋转和缩放。就像Actor组件一样,Scene组件本身不会被渲染,但可以使用它们的变换进行各种操作,比如在Actor的固定偏移处生成其他对象。

如何做...

  1. 创建一个名为ActorSpawnerComponent的自定义SceneComponent。对头文件进行以下更改:
UFUNCTION()
void Spawn();
UPROPERTY()
TSubclassOf<AActor> ActorToSpawn;
  1. 将以下函数实现添加到 cpp 文件中:
void UActorSpawnerComponent::Spawn()
{
  UWorld* TheWorld = GetWorld();
  if (TheWorld != nullptr)
  {
    FTransform ComponentTransform(this->GetComponentTransform());
    TheWorld->SpawnActor(ActorToSpawn,&ComponentTransform);
  }
}
  1. 根据此片段验证您的代码:
ActorSpawnerComponent.h
#pragma once

#include "Components/SceneComponent.h"
#include "ActorSpawnerComponent.generated.h"

UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class UE4COOKBOOK_API UActorSpawnerComponent : public USceneComponent
{
  GENERATED_BODY()

  public:
  UActorSpawnerComponent();

  virtual void BeginPlay() override;

  virtual void TickComponent( float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction ) override;

  UFUNCTION(BlueprintCallable, Category=Cookbook)
  void Spawn();

  UPROPERTY(EditAnywhere)
  TSubclassOf<AActor> ActorToSpawn;

};
ActorSpawnerComponent.cpp
#include "UE4Cookbook.h"
#include "ActorSpawnerComponent.h"

UActorSpawnerComponent::UActorSpawnerComponent()
{
  bWantsBeginPlay = true;
  PrimaryComponentTick.bCanEverTick = true;
}

void UActorSpawnerComponent::BeginPlay()
{
  Super::BeginPlay();
}

void UActorSpawnerComponent::TickComponent( float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction )
{
  Super::TickComponent( DeltaTime, TickType, ThisTickFunction );
}

void UActorSpawnerComponent::Spawn()
{
  UWorld* TheWorld = GetWorld();
  if (TheWorld != nullptr)
  {
    FTransform ComponentTransform(this->GetComponentTransform());
    TheWorld->SpawnActor(ActorToSpawn,&ComponentTransform);
  }
}
  1. 编译并打开您的项目。将一个空的Actor拖到场景中,并将ActorSpawnerComponent添加到其中。在详细信息面板中选择您的新组件,并为ActorToSpawn分配一个值。现在,每当在组件的实例上调用Spawn()时,它将实例化ActorToSpawn中指定的Actor类的副本。

它是如何工作的...

  1. 我们创建Spawn UFUNCTION和一个名为ActorToSpawn的变量。ActorToSpawnUPROPERTY类型是TSubclassOf<>,这是一个模板类型,允许我们将指针限制为基类或其子类。这也意味着在编辑器中,我们将获得一个经过预过滤的类列表可供选择,防止我们意外分配无效值。它是如何工作的...

  2. Spawn函数的实现中,我们可以访问我们的世界,并检查其有效性。

  3. SpawnActor需要一个FTransform*来指定生成新 Actor 的位置,因此我们创建一个新的堆栈变量来包含当前组件变换的副本。

  4. 如果TheWorld有效,我们请求它生成一个ActorToSpawn指定的子类的实例,传入我们刚刚创建的FTransform的地址,其中现在包含了新Actor所需的位置。

另请参阅

  • 第八章,“集成 C++和虚幻编辑器”,包含了对如何使蓝图可访问的更详细的调查。

创建自定义基本组件

Primitive组件是最复杂的Actor组件类型,因为它们不仅有一个变换,而且还在屏幕上呈现。

操作步骤...

  1. 基于MeshComponent创建一个自定义的 C++类。当 Visual Studio 加载时,将以下内容添加到你的类头文件中:
UCLASS(ClassGroup=Experimental, meta = (BlueprintSpawnableComponent))
public:
virtual FPrimitiveSceneProxy* CreateSceneProxy() override;
TArray<int32> Indices;
TArray<FVector> Vertices;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Materials)
UMaterial* TheMaterial;
  1. 我们需要在 cpp 文件中为我们重写的CreateSceneProxy函数创建一个实现:
FPrimitiveSceneProxy* UMyMeshComponent::CreateSceneProxy()
{
  FPrimitiveSceneProxy* Proxy = NULL;
  Proxy = new FMySceneProxy(this);
  return Proxy;
}
  1. 这个函数返回一个FMySceneProxy的实例,我们需要实现它。通过在CreateSceneProxy函数上方添加以下代码来实现:
class FMySceneProxy : public FPrimitiveSceneProxy
{
  public:
  FMySceneProxy(UMyMeshComponent* Component)
  :FPrimitiveSceneProxy(Component),
  Indices(Component->Indices),
  TheMaterial(Component->TheMaterial)
  {
    VertexBuffer = FMyVertexBuffer();
    IndexBuffer = FMyIndexBuffer();
    for (FVector Vertex : Component->Vertices)
    {
      Vertices.Add(FDynamicMeshVertex(Vertex));
    }
  };
  UPROPERTY()
  UMaterial* TheMaterial;
  virtual FPrimitiveViewRelevance GetViewRelevance(const FSceneView* View)  const override
  {
    FPrimitiveViewRelevance Result;
    Result.bDynamicRelevance = true;
    Result.bDrawRelevance = true;
    Result.bNormalTranslucencyRelevance = true;
    return Result;
  }
  virtual void GetDynamicMeshElements(const TArray<const FSceneView*>& Views, const FSceneViewFamily& ViewFamily, uint32 VisibilityMap, FMeshElementCollector& Collector) const override
  {
    for (int32 ViewIndex = 0; ViewIndex < Views.Num(); ViewIndex++)
    {
      FDynamicMeshBuilder MeshBuilder;
      if (Vertices.Num() == 0)
      {
        return;
      }
      MeshBuilder.AddVertices(Vertices);
      MeshBuilder.AddTriangles(Indices);
      MeshBuilder.GetMesh(FMatrix::Identity, new FColoredMaterialRenderProxy(TheMaterial->GetRenderProxy(false), FLinearColor::Gray), GetDepthPriorityGroup(Views[ViewIndex]), true, true, ViewIndex, Collector);
    }
  }
  uint32 FMySceneProxy::GetMemoryFootprint(void) const override
  {
    return sizeof(*this);
  }
  virtual ~FMySceneProxy() {};
  private:
  TArray<FDynamicMeshVertex> Vertices;
  TArray<int32> Indices;
  FMyVertexBuffer VertexBuffer;
  FMyIndexBuffer IndexBuffer;
};
  1. 我们的场景代理需要一个顶点缓冲区和一个索引缓冲区。以下子类应该放在场景代理的实现之上:
class FMyVertexBuffer : public FVertexBuffer
{
  public:
  TArray<FVector> Vertices;
  virtual void InitRHI() override
  {
    FRHIResourceCreateInfo CreateInfo;
    VertexBufferRHI = RHICreateVertexBuffer(Vertices.Num() * sizeof(FVector), BUF_Static, CreateInfo);
    void* VertexBufferData = RHILockVertexBuffer(VertexBufferRHI, 0, Vertices.Num() * sizeof(FVector), RLM_WriteOnly);
    FMemory::Memcpy(VertexBufferData, Vertices.GetData(), Vertices.Num() * sizeof(FVector));
    RHIUnlockVertexBuffer(VertexBufferRHI);
  }
};
class FMyIndexBuffer : public FIndexBuffer
{
  public:
  TArray<int32> Indices;
  virtual void InitRHI() override
  {
    FRHIResourceCreateInfo CreateInfo;
    IndexBufferRHI = RHICreateIndexBuffer(sizeof(int32), Indices.Num() * sizeof(int32), BUF_Static, CreateInfo);
    void* Buffer = RHILockIndexBuffer(IndexBufferRHI, 0, Indices.Num() * sizeof(int32), RLM_WriteOnly);
    FMemory::Memcpy(Buffer, Indices.GetData(), Indices.Num() * sizeof(int32));
    RHIUnlockIndexBuffer(IndexBufferRHI);
  }
};
  1. 添加以下构造函数实现:
UMyMeshComponent::UMyMeshComponent()
{
  static ConstructorHelpers::FObjectFinder<UMaterial> Material(TEXT("Material'/Engine/BasicShapes/BasicShapeMaterial'"));
  if (Material.Object != NULL)
  {
    TheMaterial = (UMaterial*)Material.Object;
  }
  Vertices.Add(FVector(10, 0, 0));
  Vertices.Add(FVector(0, 10, 0));
  Vertices.Add(FVector(0, 0, 10));
  Indices.Add(0);
  Indices.Add(1);
  Indices.Add(2);
}
  1. 验证你的代码是否如下所示:
#pragma once

#include "Components/MeshComponent.h"
#include "MyMeshComponent.generated.h"

UCLASS(ClassGroup = Experimental, meta = (BlueprintSpawnableComponent))
class UE4COOKBOOK_API UMyMeshComponent : public UMeshComponent
{
  GENERATED_BODY()
  public:
  virtual FPrimitiveSceneProxy* CreateSceneProxy() override;
  TArray<int32> Indices;
  TArray<FVector> Vertices;

  UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Materials)
  UMaterial* TheMaterial;
  UMyMeshComponent();
};

#include "UE4Cookbook.h"
#include "MyMeshComponent.h"
#include <VertexFactory.h>
#include "DynamicMeshBuilder.h"

class FMyVertexBuffer : public FVertexBuffer
{
  public:
  TArray<FVector> Vertices;

  virtual void InitRHI() override
  {
    FRHIResourceCreateInfo CreateInfo;
    VertexBufferRHI = RHICreateVertexBuffer(Vertices.Num() * sizeof(FVector), BUF_Static, CreateInfo);

    void* VertexBufferData = RHILockVertexBuffer(VertexBufferRHI, 0, Vertices.Num() * sizeof(FVector), RLM_WriteOnly);
    FMemory::Memcpy(VertexBufferData, Vertices.GetData(), Vertices.Num() * sizeof(FVector));
    RHIUnlockVertexBuffer(VertexBufferRHI);
  }
};

class FMyIndexBuffer : public FIndexBuffer
{
  public:
  TArray<int32> Indices;

  virtual void InitRHI() override
  {
    FRHIResourceCreateInfo CreateInfo;
    IndexBufferRHI = RHICreateIndexBuffer(sizeof(int32), Indices.Num() * sizeof(int32), BUF_Static, CreateInfo);

    void* Buffer = RHILockIndexBuffer(IndexBufferRHI, 0, Indices.Num() * sizeof(int32), RLM_WriteOnly);
    FMemory::Memcpy(Buffer, Indices.GetData(), Indices.Num() * sizeof(int32));
    RHIUnlockIndexBuffer(IndexBufferRHI);
  }
};
class FMySceneProxy : public FPrimitiveSceneProxy
{
  public:
  FMySceneProxy(UMyMeshComponent* Component)
  :FPrimitiveSceneProxy(Component),
  Indices(Component->Indices),
  TheMaterial(Component->TheMaterial)
  {
    VertexBuffer = FMyVertexBuffer();
    IndexBuffer = FMyIndexBuffer();

    for (FVector Vertex : Component->Vertices)
    {
      Vertices.Add(FDynamicMeshVertex(Component->GetComponentLocation() + Vertex));
    }
  };

UPROPERTY()
  UMaterial* TheMaterial;

  virtual FPrimitiveViewRelevance GetViewRelevance(const FSceneView* View)  const override
  {
    FPrimitiveViewRelevance Result;
    Result.bDynamicRelevance = true;
    Result.bDrawRelevance = true;
    Result.bNormalTranslucencyRelevance = true;
    return Result;
  }

  virtual void GetDynamicMeshElements(const TArray<const FSceneView*>& Views, const FSceneViewFamily& ViewFamily, uint32 VisibilityMap, FMeshElementCollector& Collector) const override
  {
    for (int32 ViewIndex = 0; ViewIndex < Views.Num(); ViewIndex++)
    {
      FDynamicMeshBuilder MeshBuilder;
      if (Vertices.Num() == 0)
      {
        return;
      }
      MeshBuilder.AddVertices(Vertices);
      MeshBuilder.AddTriangles(Indices);

      MeshBuilder.GetMesh(FMatrix::Identity, new FColoredMaterialRenderProxy(TheMaterial->GetRenderProxy(false), FLinearColor::Gray), GetDepthPriorityGroup(Views[ViewIndex]), true, true, ViewIndex, Collector);

    }
  }

  void FMySceneProxy::OnActorPositionChanged() override
  {
    VertexBuffer.ReleaseResource();
    IndexBuffer.ReleaseResource();
  }

  uint32 FMySceneProxy::GetMemoryFootprint(void) const override
  {
    return sizeof(*this);
  }
  virtual ~FMySceneProxy() {};
  private:
  TArray<FDynamicMeshVertex> Vertices;
  TArray<int32> Indices;
  FMyVertexBuffer VertexBuffer;
  FMyIndexBuffer IndexBuffer;
};

FPrimitiveSceneProxy* UMyMeshComponent::CreateSceneProxy()
{
  FPrimitiveSceneProxy* Proxy = NULL;
  Proxy = new FMySceneProxy(this);
  return Proxy;
}

UMyMeshComponent::UMyMeshComponent()
{
  static ConstructorHelpers::FObjectFinder<UMaterial> Material(TEXT("Material'/Engine/BasicShapes/BasicShapeMaterial'"));

  if (Material.Object != NULL)
  {
    TheMaterial = (UMaterial*)Material.Object;
  }
  Vertices.Add(FVector(10, 0, 0));
  Vertices.Add(FVector(0, 10, 0));
  Vertices.Add(FVector(0, 0, 10));
  Indices.Add(0);
  Indices.Add(1);
  Indices.Add(2);
}
  1. 在编辑器中创建一个空的Actor,并将新的网格组件添加到其中,以查看你的三角形是否被渲染。尝试通过更改添加到顶点的值来进行实验。添加并查看在重新编译后几何图形如何改变。操作步骤

它是如何工作的...

  1. 为了渲染一个Actor,描述它的数据需要被传递给渲染线程。

  2. 最简单的方法是使用场景代理-在渲染线程上创建的代理对象,旨在为数据传输提供线程安全性。

  3. PrimitiveComponent类定义了一个CreateSceneProxy函数,返回FPrimitiveSceneProxy*。这个函数允许像我们这样的自定义组件返回一个基于FPrimitiveSceneProxy的对象,利用多态性。

  4. 我们定义了SceneProxy对象的构造函数,以便每个创建的SceneProxy都知道与其关联的组件实例。

  5. 然后这些数据被缓存在场景代理中,并使用GetDynamicMeshElements传递给渲染器。

  6. 我们创建了一个IndexBuffer和一个VertexBuffer。我们创建的每个缓冲区类都是辅助类,帮助场景代理为这两个缓冲区分配特定于平台的内存。它们在InitRHI(也称为初始化渲染硬件接口)函数中这样做,在这个函数中,它们使用 RHI API 的函数来创建一个顶点缓冲区,锁定它,复制所需的数据,然后解锁它。

  7. 在组件的构造函数中,我们使用ObjectFinder模板查找内置在引擎中的材质资源,以便我们的网格有一个材质。

  8. 然后我们向我们的缓冲区添加一些顶点和索引,以便在渲染器请求场景代理时可以绘制网格。

为 RPG 创建一个 InventoryComponent

一个InventoryComponent使其包含的Actor能够在其库存中存储InventoryActors,并将它们放回游戏世界中。

准备工作

在继续本教程之前,请确保你已经按照第六章,“输入和碰撞”,中的轴映射-键盘、鼠标和游戏手柄方向输入用于 FPS 角色教程中的步骤进行操作,因为它向你展示了如何创建一个简单的角色。

此外,本章中的使用 SpawnActor 实例化 Actor教程向你展示了如何创建一个自定义的GameMode

操作步骤...

  1. 使用引擎创建一个ActorComponent子类,名为InventoryComponent,然后将以下代码添加到其中:
UPROPERTY()
TArray<AInventoryActor*> CurrentInventory;
UFUNCTION()
int32 AddToInventory(AInventoryActor* ActorToAdd);

UFUNCTION()
void RemoveFromInventory(AInventoryActor* ActorToRemove);
  1. 将以下函数实现添加到源文件中:
int32 UInventoryComponent::AddToInventory(AInventoryActor* ActorToAdd)
{
  return CurrentInventory.Add(ActorToAdd);
}

void UInventoryComponent::RemoveFromInventory(AInventoryActor* ActorToRemove)
{
  CurrentInventory.Remove(ActorToRemove);
}
  1. 接下来,创建一个名为InventoryActor的新StaticMeshActor子类。将以下内容添加到其声明中:
virtual void PickUp();
virtual void PutDown(FTransform TargetLocation);
  1. 在实现文件中实现新函数:
void AInventoryActor::PickUp()
{
  SetActorTickEnabled(false);
  SetActorHiddenInGame(true);
  SetActorEnableCollision(false);
}

void AInventoryActor::PutDown(FTransform TargetLocation)
{
  SetActorTickEnabled(true);
  SetActorHiddenInGame(false);
  SetActorEnableCollision(true);
  SetActorLocation(TargetLocation.GetLocation());
}
  1. 还要更改构造函数如下:
AInventoryActor::AInventoryActor()
:Super()
{
  PrimaryActorTick.bCanEverTick = true;
  auto MeshAsset = ConstructorHelpers::FObjectFinder<UStaticMesh>(TEXT("StaticMesh'/Engine/BasicShapes/Cube.Cube'"));
  if (MeshAsset.Object != nullptr)
  {
    GetStaticMeshComponent()->SetStaticMesh(MeshAsset.Object);
    GetStaticMeshComponent()->SetCollisionProfileName(UCollisionProfile::Pawn_ProfileName);
  }
  GetStaticMeshComponent()->SetMobility(EComponentMobility::Movable);
  SetActorEnableCollision(true);
}
  1. 我们需要向角色添加InventoryComponent,以便我们有一个可以存储物品的库存。使用编辑器创建一个新的SimpleCharacter子类,并将以下内容添加到其声明中:
UPROPERTY()
UInventoryComponent* MyInventory;

UFUNCTION()
virtual void SetupPlayerInputComponent(class UInputComponent* InputComponent) override;

UFUNCTION()
void DropItem();
UFUNCTION()
void TakeItem(AInventoryActor* InventoryItem);

UFUNCTION()
virtual void NotifyHit(class UPrimitiveComponent* MyComp, AActor* Other, class UPrimitiveComponent* OtherComp, bool bSelfMoved, FVector HitLocation, FVector HitNormal, FVector NormalImpulse, const FHitResult& Hit) override;
  1. 将此行添加到角色的构造函数实现中:
MyInventory = CreateDefaultSubobject<UInventoryComponent>("MyInventory");
  1. 将此代码添加到重写的SetupPlayerInputComponent中:
void AInventoryCharacter::SetupPlayerInputComponent(class UInputComponent* InputComponent)
{
  Super::SetupPlayerInputComponent(InputComponent);
  InputComponent->BindAction("DropItem", EInputEvent::IE_Pressed, this, &AInventoryCharacter::DropItem);
}
  1. 最后,添加以下函数实现:
void AInventoryCharacter::DropItem()
{
  if (MyInventory->CurrentInventory.Num() == 0)
  {
    return;
  }

  AInventoryActor* Item = MyInventory->CurrentInventory.Last();
  MyInventory->RemoveFromInventory(Item);
  FVector ItemOrigin;
  FVector ItemBounds;
  Item->GetActorBounds(false, ItemOrigin, ItemBounds);
  FTransform PutDownLocation = GetTransform() + FTransform(RootComponent->GetForwardVector() * ItemBounds.GetMax());
  Item->PutDown(PutDownLocation);
}

void AInventoryCharacter::NotifyHit(class UPrimitiveComponent* MyComp, AActor* Other, class UPrimitiveComponent* OtherComp, bool bSelfMoved, FVector HitLocation, FVector HitNormal, FVector NormalImpulse, const FHitResult& Hit)
{
  AInventoryActor* InventoryItem = Cast<AInventoryActor>(Other);
  if (InventoryItem != nullptr)
  {
    TakeItem(InventoryItem);
  }
}

void AInventoryCharacter::TakeItem(AInventoryActor* InventoryItem)
{
  InventoryItem->PickUp();
  MyInventory->AddToInventory(InventoryItem);
}
  1. 编译您的代码并在编辑器中进行测试。创建一个新级别,并将几个InventoryActor实例拖到场景中。

  2. 如果需要提醒如何重写当前游戏模式,请参考使用 SpawnActor 实例化 Actor配方。将以下行添加到该配方中的游戏模式构造函数中,然后将您的级别的GameMode设置为您在该配方中创建的游戏模式:

DefaultPawnClass = AInventoryCharacter::StaticClass();
  1. 在编译和启动项目之前,请对照此处的清单验证您的代码。
#pragma once

#include "GameFramework/Character.h"
#include "InventoryComponent.h"
#include "InventoryCharacter.generated.h"

UCLASS()
class UE4COOKBOOK_API AInventoryCharacter : public ACharacter
{
  GENERATED_BODY()

  public:
  AInventoryCharacter();
  virtual void BeginPlay() override;
  virtual void Tick( float DeltaSeconds ) override;
  virtual void SetupPlayerInputComponent(class UInputComponent* InputComponent) override;

  UPROPERTY()
  UInventoryComponent* MyInventory;
  UPROPERTY()
  UCameraComponent* MainCamera;
  UFUNCTION()
  void TakeItem(AInventoryActor* InventoryItem);
  UFUNCTION()
  void DropItem();
  void MoveForward(float AxisValue);
  void MoveRight(float AxisValue);
  void PitchCamera(float AxisValue);
  void YawCamera(float AxisValue);

  UFUNCTION()
  virtual void NotifyHit(class UPrimitiveComponent* MyComp, AActor* Other, class UPrimitiveComponent* OtherComp, bool bSelfMoved, FVector HitLocation, FVector HitNormal, FVector NormalImpulse, const FHitResult& Hit) override;
  private:
  FVector MovementInput;
  FVector CameraInput;
};

#include "UE4Cookbook.h"
#include "InventoryCharacter.h"

AInventoryCharacter::AInventoryCharacter()
:Super()
{
  PrimaryActorTick.bCanEverTick = true;
  MyInventory = CreateDefaultSubobject<UInventoryComponent>("MyInventory");
  MainCamera = CreateDefaultSubobject<UCameraComponent>("MainCamera");
  MainCamera->bUsePawnControlRotation = 0;
}

void AInventoryCharacter::BeginPlay()
{
  Super::BeginPlay();
  MainCamera->AttachTo(RootComponent);
}

void AInventoryCharacter::Tick( float DeltaTime )
{
  Super::Tick( DeltaTime );
  if (!MovementInput.IsZero())
  {
    MovementInput *= 100;
    FVector InputVector = FVector(0,0,0);
    InputVector += GetActorForwardVector()* MovementInput.X * DeltaTime;
    InputVector += GetActorRightVector()* MovementInput.Y * DeltaTime;
    GetCharacterMovement()->AddInputVector(InputVector);
    GEngine->AddOnScreenDebugMessage(-1, 1, FColor::Red, FString::Printf(TEXT("x- %f, y - %f, z - %f"),InputVector.X, InputVector.Y, InputVector.Z));
  }

  if (!CameraInput.IsNearlyZero())
  {
    FRotator NewRotation = GetActorRotation();
    NewRotation.Pitch += CameraInput.Y;
    NewRotation.Yaw += CameraInput.X;
    APlayerController* MyPlayerController =Cast<APlayerController>(GetController());
    if (MyPlayerController != nullptr)
    {
      MyPlayerController->AddYawInput(CameraInput.X);
      MyPlayerController->AddPitchInput(CameraInput.Y);
    }
    SetActorRotation(NewRotation);
  }
}
void AInventoryCharacter::SetupPlayerInputComponent(class UInputComponent* InputComponent)
{
  Super::SetupPlayerInputComponent(InputComponent);
  InputComponent->BindAxis("MoveForward", this, &AInventoryCharacter::MoveForward);
  InputComponent->BindAxis("MoveRight", this, &AInventoryCharacter::MoveRight);
  InputComponent->BindAxis("CameraPitch", this, &AInventoryCharacter::PitchCamera);
  InputComponent->BindAxis("CameraYaw", this, &AInventoryCharacter::YawCamera);
  InputComponent->BindAction("DropItem", EInputEvent::IE_Pressed, this, &AInventoryCharacter::DropItem);
}
void AInventoryCharacter::DropItem()
{
  if (MyInventory->CurrentInventory.Num() == 0)
  {
    return;
  }
  AInventoryActor* Item = MyInventory->CurrentInventory.Last();
  MyInventory->RemoveFromInventory(Item);
  FVector ItemOrigin;
  FVector ItemBounds;
  Item->GetActorBounds(false, ItemOrigin, ItemBounds);
  FTransform PutDownLocation = GetTransform() + FTransform(RootComponent->GetForwardVector() * ItemBounds.GetMax());
  Item->PutDown(PutDownLocation);
}

void AInventoryCharacter::MoveForward(float AxisValue)
{
  MovementInput.X = FMath::Clamp<float>(AxisValue, -1.0f, 1.0f);
}

void AInventoryCharacter::MoveRight(float AxisValue)
{
  MovementInput.Y = FMath::Clamp<float>(AxisValue, -1.0f, 1.0f);
}

void AInventoryCharacter::PitchCamera(float AxisValue)
{
  CameraInput.Y = AxisValue;
}
void AInventoryCharacter::YawCamera(float AxisValue)
{
  CameraInput.X = AxisValue;
}
void AInventoryCharacter::NotifyHit(class UPrimitiveComponent* MyComp, AActor* Other, class UPrimitiveComponent* OtherComp, bool bSelfMoved, FVector HitLocation, FVector HitNormal, FVector NormalImpulse, const FHitResult& Hit)
{
  AInventoryActor* InventoryItem = Cast<AInventoryActor>(Other);
  if (InventoryItem != nullptr)
  {
    TakeItem(InventoryItem);
  }
}
void AInventoryCharacter::TakeItem(AInventoryActor* InventoryItem)
{
  InventoryItem->PickUp();
  MyInventory->AddToInventory(InventoryItem);
}

#pragma once

#include "Components/ActorComponent.h"
#include "InventoryActor.h"
#include "InventoryComponent.generated.h"

UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
class UE4COOKBOOK_API UInventoryComponent : public UActorComponent
{
  GENERATED_BODY()

  public:
  UInventoryComponent();
  virtual void TickComponent( float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction ) override;

  UPROPERTY()
  TArray<AInventoryActor*> CurrentInventory;
  UFUNCTION()
  int32 AddToInventory(AInventoryActor* ActorToAdd);

  UFUNCTION()
  void RemoveFromInventory(AInventoryActor* ActorToRemove);
};
#include "UE4Cookbook.h"
#include "InventoryComponent.h"

UInventoryComponent::UInventoryComponent()
{
  bWantsBeginPlay = true;
  PrimaryComponentTick.bCanEverTick = true;
}
void UInventoryComponent::TickComponent( float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction )
{
  Super::TickComponent( DeltaTime, TickType, ThisTickFunction );
}

int32 UInventoryComponent::AddToInventory(AInventoryActor* ActorToAdd)
{
  return CurrentInventory.Add(ActorToAdd);
}

void UInventoryComponent::RemoveFromInventory(AInventoryActor* ActorToRemove)
{
  CurrentInventory.Remove(ActorToRemove);
}

#pragma once

#include "GameFramework/GameMode.h"
#include "UE4CookbookGameMode.generated.h"

UCLASS()
class UE4COOKBOOK_API AUE4CookbookGameMode : public AGameMode
{
  GENERATED_BODY()

  public:
  AUE4CookbookGameMode();
  };

#include "UE4Cookbook.h"
#include "MyGameState.h"
#include "InventoryCharacter.h"
#include "UE4CookbookGameMode.h"

AUE4CookbookGameMode::AUE4CookbookGameMode()
{
  DefaultPawnClass = AInventoryCharacter::StaticClass();
  GameStateClass = AMyGameState::StaticClass();
}
  1. 最后,我们需要在编辑器中的绑定中添加我们的InputAction。为此,通过选择Edit | Project Settings...来打开Project Settings...窗口:如何做...

然后,在左侧选择Input。选择Action Mappings旁边的加号符号,并在出现的文本框中键入DropItem。在其下是您可以绑定到此操作的所有潜在按键的列表。选择标记为E的按键。您的设置现在应如下所示:

如何做...

  1. 然后我们可以点击播放,走到我们的库存角色旁边,它将被拾起。按E键将角色放置在新位置!通过多个库存角色测试,看它们是否都被正确收集和放置。

工作原理...

  1. 我们的新组件包含一个存储指针的角色数组,以及声明添加或移除项目到数组的函数。这些函数是围绕TArray的添加/移除功能的简单包装器,但允许我们选择性地执行诸如在继续存储项目之前检查数组是否在指定大小限制内等操作。

  2. InventoryActor是一个基类,可用于玩家拿走的所有物品。

  3. PickUp函数中,我们需要在拾起时禁用角色。为此,我们必须执行以下操作:

  • 禁用角色打勾

  • 隐藏角色

  • 禁用碰撞

  1. 我们使用SetActorTickEnabledSetActorHiddenInGameSetActorEnableCollision函数来实现这一点。

  2. PutDown函数是相反的。我们启用角色打勾,取消隐藏角色,然后重新打开其碰撞,并将角色传送到所需位置。

  3. 我们还在新角色中添加了InventoryComponent以及一个用于获取物品的函数。

  4. 在我们角色的构造函数中,我们为我们的InventoryComponent创建了一个默认子对象。

  5. 我们还添加了一个NotifyHit覆盖,以便在角色撞到其他角色时得到通知。

  6. 在此函数中,我们将其他角色转换为InventoryActor。如果转换成功,那么我们知道我们的Actor是一个InventoryActor,因此我们可以调用TakeItem函数来拿起它。

  7. TakeItem函数中,我们通知库存物品角色我们要拿起它,然后将其添加到我们的库存中。

  8. InventoryCharacter中的最后一个功能是DropItem函数。此函数检查我们的库存中是否有任何物品。如果有任何物品,我们将其从库存中移除,然后使用物品边界计算我们的玩家角色前方的安全距离,以便放下物品。

  9. 然后,我们通知物品我们正在将其放置在所需位置的世界中。

另请参阅

  • 第五章, 处理事件和委托,详细解释了事件和输入处理在引擎中如何一起工作,以及本教程中提到的SimpleCharacter类的用法。

  • 第六章, 输入和碰撞,还有关于绑定输入动作和轴的教程

创建一个 OrbitingMovement 组件

这个组件类似于RotatingMovementComponent,它旨在使附加到它的组件以特定方式移动。在这种情况下,它将以固定距离围绕固定点移动任何附加的组件。

例如,这可以用于动作 RPG中围绕角色旋转的护盾。

操作步骤...

  1. 创建一个新的SceneComponent子类,并将以下属性添加到类声明中:
UPROPERTY()
bool RotateToFaceOutwards;
UPROPERTY()
float RotationSpeed;
UPROPERTY()
float OrbitDistance;
float CurrentValue;
  1. 将以下内容添加到构造函数中:
RotationSpeed = 5;
OrbitDistance = 100;
CurrentValue = 0;
RotateToFaceOutwards = true;
  1. 将以下代码添加到TickComponent函数中:
float CurrentValueInRadians = FMath::DegreesToRadians<float>(CurrentValue);
SetRelativeLocation(FVector(OrbitDistance * FMath::Cos(CurrentValueInRadians), OrbitDistance * FMath::Sin(CurrentValueInRadians), RelativeLocation.Z));
if (RotateToFaceOutwards)
{
  FVector LookDir = (RelativeLocation).GetSafeNormal();
  FRotator LookAtRot = LookDir.Rotation();
  SetRelativeRotation(LookAtRot);
}
CurrentValue = FMath::Fmod(CurrentValue + (RotationSpeed* DeltaTime) ,360);
  1. 根据以下清单验证你的工作:
#pragma once
#include "Components/SceneComponent.h"
#include "OrbitingMovementComponent.generated.h"

UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class UE4COOKBOOK_API UOrbitingMovementComponent : public USceneComponent
{
  GENERATED_BODY()
  public:
  // Sets default values for this component's properties
  UOrbitingMovementComponent();

  // Called when the game starts
  virtual void BeginPlay() override;
  // Called every frame
  virtual void TickComponent( float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction ) override;

  UPROPERTY()
  bool RotateToFaceOutwards;
  UPROPERTY()
  float RotationSpeed;
  UPROPERTY()
  float OrbitDistance;
  float CurrentValue;
};
#include "UE4Cookbook.h"
#include "OrbitingMovementComponent.h"
// Sets default values for this component's properties
UOrbitingMovementComponent::UOrbitingMovementComponent()
{
  // Set this component to be initialized when the game starts, and to be ticked every frame. You can turn these features
  // off to improve performance if you don't need them.
  bWantsBeginPlay = true;
  PrimaryComponentTick.bCanEverTick = true;
  RotationSpeed = 5;
  OrbitDistance = 100;
  CurrentValue = 0;
  RotateToFaceOutwards = true;
  //...
}

// Called when the game starts
void UOrbitingMovementComponent::BeginPlay()
{
  Super::BeginPlay();
  //...
}
// Called every frame
void UOrbitingMovementComponent::TickComponent( float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction )
{
  Super::TickComponent( DeltaTime, TickType, ThisTickFunction );
  float CurrentValueInRadians = FMath::DegreesToRadians<float>(CurrentValue);
  SetRelativeLocation(
  FVector(OrbitDistance * FMath::Cos(CurrentValueInRadians),
  OrbitDistance * FMath::Sin(CurrentValueInRadians),
  RelativeLocation.Z));
  if (RotateToFaceOutwards)
  {
    FVector LookDir = (RelativeLocation).GetSafeNormal();
    FRotator LookAtRot = LookDir.Rotation();
    SetRelativeRotation(LookAtRot);
  }
  CurrentValue = FMath::Fmod(CurrentValue + (RotationSpeed* DeltaTime) ,360);
  //...
}
  1. 你可以通过创建一个简单的Actor蓝图来测试这个组件。

  2. 将一个OrbitingMovement组件添加到你的Actor中,然后使用Cube组件添加一些网格。通过将它们拖放到Components面板中的OrbitingMovement组件上,将它们作为子组件。最终的层次结构应该如下所示:How to do it...

  3. 如果你对这个过程不确定,可以参考创建自定义 Actor 组件教程。

  4. 点击播放,看看网格是否围绕Actor中心以圆周运动。

工作原理...

  1. 添加到组件的属性是我们用来自定义组件的圆周运动的基本参数。

  2. RotateToFaceOutwards指定组件是否在每次更新时朝向远离旋转中心。RotationSpeed是组件每秒旋转的度数。

  3. OrbitDistance表示旋转的组件必须从原点移动的距离。CurrentValue是当前的旋转位置(以度为单位)。

  4. 在我们的构造函数中,我们为我们的新组件建立了一些合理的默认值。

  5. TickComponent函数中,我们计算我们组件的位置和旋转。

  6. 下一步的公式要求我们的角度用弧度而不是度来表示。弧度用 π 来描述角度。我们首先使用DegreesToRadians函数将我们当前的度数值转换为弧度。

  7. SetRelativeLocation函数使用了圆周运动的一般方程,即 Pos(θ) = cos(θ in radians), sin(θ in radians)。我们保留每个对象的 Z 轴位置。

  8. 下一步是将对象旋转回原点(或者直接远离原点)。只有当RotateToFaceOutwardstrue时才会计算这一步,它涉及到获取组件相对于其父级的相对偏移,并创建一个基于从父级指向当前相对偏移的向量的旋转器。然后我们将相对旋转设置为结果旋转器。

  9. 最后,我们增加当前的度数值,使其每秒移动RotationSpeed单位,将结果值夹在 0 和 360 之间,以允许旋转循环。

创建一个生成单位的建筑

对于这个教程,我们将创建一个在特定位置定时生成单位的建筑。

操作步骤...

  1. 在编辑器中创建一个新的Actor子类,然后将以下实现添加到类中:
UPROPERTY()
UStaticMeshComponent* BuildingMesh;
UPROPERTY()
UParticleSystemComponent* SpawnPoint;

UPROPERTY()
UClass* UnitToSpawn;

UPROPERTY()
float SpawnInterval;

UFUNCTION()
void SpawnUnit();

UFUNCTION()
void EndPlay(const EEndPlayReason::Type EndPlayReason) override;

UPROPERTY()
FTimerHandle SpawnTimerHandle;
  1. 将以下内容添加到构造函数中:
BuildingMesh = CreateDefaultSubobject<UStaticMeshComponent>("BuildingMesh");
SpawnPoint = CreateDefaultSubobject<UParticleSystemComponent>("SpawnPoint");
SpawnInterval = 10;
auto MeshAsset = ConstructorHelpers::FObjectFinder<UStaticMesh>(TEXT("StaticMesh'/Engine/BasicShapes/Cube.Cube'"));
if (MeshAsset.Object != nullptr)
{
  BuildingMesh->SetStaticMesh(MeshAsset.Object);
  BuildingMesh->SetCollisionProfileName(UCollisionProfile::Pawn_ProfileName);

}
auto ParticleSystem =
ConstructorHelpers::FObjectFinder<UParticleSystem>(TEXT("ParticleSystem'/Engine/Tutorial/SubEditors/TutorialAssets/TutorialParticleSystem.TutorialParticleSystem'"));
if (ParticleSystem.Object != nullptr)
{
  SpawnPoint->SetTemplate(ParticleSystem.Object);
}
SpawnPoint->SetRelativeScale3D(FVector(0.5, 0.5, 0.5));
UnitToSpawn = ABarracksUnit::StaticClass();
  1. 将以下内容添加到BeginPlay函数中:
RootComponent = BuildingMesh;
SpawnPoint->AttachTo(RootComponent);
SpawnPoint->SetRelativeLocation(FVector(150, 0, 0));
GetWorld()->GetTimerManager().SetTimer(SpawnTimerHandle, this, &ABarracks::SpawnUnit, SpawnInterval, true);
  1. SpawnUnit函数创建实现:
void ABarracks::SpawnUnit()
{
  FVector SpawnLocation = SpawnPoint->GetComponentLocation();
  GetWorld()->SpawnActor(UnitToSpawn, &SpawnLocation);
}
  1. 实现重写的EndPlay函数:
void ABarracks::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
  Super::EndPlay(EndPlayReason);
  GetWorld()->GetTimerManager().ClearTimer(SpawnTimerHandle);
}
  1. 接下来,创建一个新的角色子类,并添加一个属性:
UPROPERTY()
UParticleSystemComponent* VisualRepresentation;
  1. 在构造函数中初始化组件:
VisualRepresentation = CreateDefaultSubobject<UParticleSystemComponent>("SpawnPoint");
auto ParticleSystem =ConstructorHelpers::FObjectFinder<UParticleSystem>(TEXT("ParticleSystem'/Engine/Tutorial/SubEditors/TutorialAssets/TutorialParticleSystem.TutorialParticleSystem'"));
if (ParticleSystem.Object != nullptr)
{
  SpawnPoint->SetTemplate(ParticleSystem.Object);
}
SpawnPoint->SetRelativeScale3D(FVector(0.5, 0.5, 0.5));
SpawnCollisionHandlingMethod = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
  1. 将可视化表示附加到根组件:
void ABarracksUnit::BeginPlay()
{
  Super::BeginPlay();
  SpawnPoint->AttachTo(RootComponent);
}
  1. 最后,将以下内容添加到 Tick 函数中以使生成的角色移动:
SetActorLocation(GetActorLocation() + FVector(10, 0, 0));
  1. 根据以下片段进行验证,然后编译您的项目。将兵营角色的副本放入级别中。然后您可以观察它以固定间隔生成角色:
#pragma once
#include "GameFramework/Actor.h"
#include "Barracks.generated.h"
UCLASS()
class UE4COOKBOOK_API ABarracks : public AActor
{
  GENERATED_BODY()
  public:
  ABarracks();
  virtual void BeginPlay() override;
  virtual void Tick( float DeltaSeconds ) override;

  UPROPERTY()
  UStaticMeshComponent* BuildingMesh;
  UPROPERTY()
  UParticleSystemComponent* SpawnPoint;

  UPROPERTY()
  UClass* UnitToSpawn;

  UPROPERTY()
  float SpawnInterval;

  UFUNCTION()
  void SpawnUnit();
  UFUNCTION()
  void EndPlay(const EEndPlayReason::Type EndPlayReason) override;

  UPROPERTY()
  FTimerHandle SpawnTimerHandle;
};

#include "UE4Cookbook.h"
#include "BarracksUnit.h"
#include "Barracks.h"

// Sets default values
ABarracks::ABarracks()
{
  // Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it.
  PrimaryActorTick.bCanEverTick = true;
  BuildingMesh = CreateDefaultSubobject<UStaticMeshComponent>("BuildingMesh");
  SpawnPoint = CreateDefaultSubobject<UParticleSystemComponent>("SpawnPoint");
  SpawnInterval = 10;
  auto MeshAsset = ConstructorHelpers::FObjectFinder<UStaticMesh>(TEXT("StaticMesh'/Engine/BasicShapes/Cube.Cube'"));
  if (MeshAsset.Object != nullptr)
  {
    BuildingMesh->SetStaticMesh(MeshAsset.Object);
    BuildingMesh->SetCollisionProfileName(UCollisionProfile::Pawn_ProfileName);

  }
  auto ParticleSystem = ConstructorHelpers::FObjectFinder<UParticleSystem>(TEXT("ParticleSystem'/Engine/Tutorial/SubEditors/TutorialAssets/TutorialParticleSystem.TutorialParticleSystem'"));
  if (ParticleSystem.Object != nullptr)
  {
    SpawnPoint->SetTemplate(ParticleSystem.Object);
  }
  SpawnPoint->SetRelativeScale3D(FVector(0.5, 0.5, 0.5));
  UnitToSpawn = ABarracksUnit::StaticClass();
}
void ABarracks::BeginPlay()
{
  Super::BeginPlay();
  RootComponent = BuildingMesh;
  SpawnPoint->AttachTo(RootComponent);
  SpawnPoint->SetRelativeLocation(FVector(150, 0, 0));
  GetWorld()->GetTimerManager().SetTimer(SpawnTimerHandle, this, &ABarracks::SpawnUnit, SpawnInterval, true);
}

void ABarracks::Tick( float DeltaTime )
{
  Super::Tick( DeltaTime );
}
void ABarracks::SpawnUnit()
{
  FVector SpawnLocation = SpawnPoint->GetComponentLocation();
  GetWorld()->SpawnActor(UnitToSpawn, &SpawnLocation);
}

void ABarracks::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
  Super::EndPlay(EndPlayReason);
  GetWorld()->GetTimerManager().ClearTimer(SpawnTimerHandle);
}

#pragma once

#include "GameFramework/Character.h"
#include "BarracksUnit.generated.h"

UCLASS()
class UE4COOKBOOK_API ABarracksUnit : public ACharacter
{
  GENERATED_BODY()

  public:
  ABarracksUnit();

  virtual void BeginPlay() override;
  virtual void Tick( float DeltaSeconds ) override;

  virtual void SetupPlayerInputComponent(class UInputComponent* InputComponent) override;

  UPROPERTY()
  UParticleSystemComponent* SpawnPoint;
};

#include "UE4Cookbook.h"
#include "BarracksUnit.h"

ABarracksUnit::ABarracksUnit()
{
  PrimaryActorTick.bCanEverTick = true;
  SpawnPoint = CreateDefaultSubobject<UParticleSystemComponent>("SpawnPoint");
  auto ParticleSystem =ConstructorHelpers::FObjectFinder<UParticleSystem>(TEXT("ParticleSystem'/Engine/Tutorial/SubEditors/TutorialAssets/TutorialParticleSystem.TutorialParticleSystem'"));
  if (ParticleSystem.Object != nullptr)
  {
    SpawnPoint->SetTemplate(ParticleSystem.Object);
  }
  SpawnPoint->SetRelativeScale3D(FVector(0.5, 0.5, 0.5));
  SpawnCollisionHandlingMethod = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
}
void ABarracksUnit::BeginPlay()
{
  Super::BeginPlay();
  SpawnPoint->AttachTo(RootComponent);
}

void ABarracksUnit::Tick( float DeltaTime )
{
  Super::Tick( DeltaTime );
  SetActorLocation(GetActorLocation() + FVector(10, 0, 0));
}
void ABarracksUnit::SetupPlayerInputComponent(class UInputComponent* InputComponent)
{
  Super::SetupPlayerInputComponent(InputComponent);
}

它是如何工作的...

  1. 首先,我们创建兵营角色。我们添加一个粒子系统组件来指示新单位将生成的位置,以及一个静态网格用于建筑的可视表示。

  2. 在构造函数中,我们初始化组件,然后使用 FObjectFinder 设置它们的值。我们还使用 StaticClass 函数设置要生成的类,以从类类型中检索 UClass* 实例。

  3. 在兵营的 BeginPlay 函数中,我们创建一个定时器,以固定间隔调用我们的 SpawnUnit 函数。我们将定时器句柄存储在类的成员变量中,这样当我们的实例被销毁时,我们可以停止定时器;否则,当定时器再次触发时,我们将遇到对象指针被取消引用的崩溃。

  4. SpawnUnit 函数获取了 SpawnPoint 对象的世界空间位置,然后请求世界在该位置生成一个我们单位类的实例。

  5. BarracksUnit 在其 Tick() 函数中有代码,每帧向前移动 10 个单位,以便每个生成的单位都会移动以为下一个单位腾出空间。

  6. EndPlay 函数重写调用父类函数的实现,如果父类中有要取消的定时器或要执行的去初始化操作,这一点很重要。然后使用存储在 BeginPlay 中的定时器句柄来取消定时器。

第五章:处理事件和委托

Unreal 使用事件有效地通知类有关游戏世界中发生的事情。事件和委托对于确保可以以最小化类耦合的方式发出这些通知以及允许任意类订阅以接收通知非常有用。

本章中将介绍以下教程:

  • 通过虚函数实现事件处理

  • 创建一个绑定到 UFUNCTION 的委托

  • 取消注册委托

  • 创建一个带有输入参数的委托

  • 使用委托绑定传递有效负载数据

  • 创建一个多播委托

  • 创建一个自定义事件

  • 创建一个时间处理程序

  • 为第一人称射击游戏创建一个重生拾取物

通过虚函数实现事件处理

Unreal 提供的一些ActorComponent类包括以虚函数形式的事件处理程序。本教程将向您展示如何通过覆盖相关的虚函数来自定义这些处理程序。

操作步骤...

  1. 在编辑器中创建一个空的Actor。将其命名为MyTriggerVolume

  2. 将以下代码添加到类头文件中:

UPROPERTY()
UBoxComponent* TriggerZone;

UFUNCTION()
virtual void NotifyActorBeginOverlap(AActor* OtherActor) override;
UFUNCTION()
virtual void NotifyActorEndOverlap(AActor* OtherActor) override;
  1. 将前述函数的实现添加到 cpp 文件中:
void AMyTriggerVolume::NotifyActorBeginOverlap(AActor* OtherActor)
{
  GEngine->AddOnScreenDebugMessage(-1, 1, FColor::Red, FString::Printf(TEXT("%s entered me"),*(OtherActor->GetName())));
}

void AMyTriggerVolume::NotifyActorEndOverlap(AActor* OtherActor)
{
  GEngine->AddOnScreenDebugMessage(-1, 1, FColor::Red, FString::Printf(TEXT("%s left me"), *(OtherActor->GetName())));
}
  1. 编译您的项目,并将MyTriggerActor的一个实例放入级别中。通过走进体积并查看屏幕上打印的输出来验证重叠/触摸事件是否已处理:操作步骤...

工作原理...

  1. 与往常一样,我们首先声明一个UPROPERTY来保存对我们组件子对象的引用。然后创建两个UFUNCTION声明。这些标记为virtualoverride,以便编译器理解我们要替换父类的实现,并且我们的函数实现可以被替换。

  2. 在函数的实现中,我们使用FString::printf从预设文本创建一个FString,并替换一些数据参数。

  3. 请注意,FString OtherActor->GetName()返回,并在传递给FString::Format之前使用*运算符进行解引用。不这样做会导致错误。

  4. 然后将此FString传递给全局引擎函数AddOnScreenDebugMessage

  5. -1的第一个参数告诉引擎允许重复字符串,第二个参数是消息显示的持续时间(以秒为单位),第三个参数是颜色,第四个参数是要打印的实际字符串。

  6. 现在,当我们的 Actor 的组件与其他物体重叠时,其UpdateOverlaps函数将调用NotifyActorBeginOverlap,并且虚函数分发将调用我们的自定义实现。

创建一个绑定到 UFUNCTION 的委托

委托允许我们调用一个函数,而不知道分配了哪个函数。它们是原始函数指针的更安全版本。本教程向您展示如何将UFUNCTION与委托关联,以便在执行委托时调用它。

准备工作

确保您已按照之前的步骤创建了一个TriggerVolume类。

操作步骤...

  1. 在我们的GameMode头文件中,在类声明之前使用以下宏声明委托:
DECLARE_DELEGATE(FStandardDelegateSignature)
UCLASS()
class UE4COOKBOOK_API AUE4CookbookGameMode : public AGameMode
  1. 向我们的游戏模式添加一个新成员:
FStandardDelegateSignature MyStandardDelegate;
  1. 创建一个名为DelegateListener的新Actor类。将以下内容添加到该类的声明中:
UFUNCTION()
void EnableLight();

UPROPERTY()
UPointLightComponent* PointLight;
  1. 在类实现中,将以下内容添加到构造函数中:
PointLight = CreateDefaultSubobject<UPointLightComponent>("PointLight");
RootComponent = PointLight;
PointLight->SetVisibility(false);
  1. DelegateListener.cpp文件中,在项目的include文件和DelegateListener头文件之间添加#include "UE4CookbookGameMode.h"。在DelegateListener::BeginPlay实现中,添加以下内容:
Super::BeginPlay();
if (TheWorld != nullptr)
{
  AGameMode* GameMode = UGameplayStatics::GetGameMode(TheWorld);
  AUE4CookbookGameMode * MyGameMode = Cast<AUE4CookbookGameMode>(GameMode);
  if (MyGameMode != nullptr)
  {
    MyGameMode->MyStandardDelegate.BindUObject(this, &ADelegateListener::EnableLight);
  }
}
  1. 最后,实现EnableLight
void ADelegateListener::EnableLight()
{
  PointLight->SetVisibility(true);
}
  1. 将以下代码放入我们的 TriggerVolume 的NotifyActorBeginOverlap函数中:
UWorld* TheWorld = GetWorld();
if (TheWorld != nullptr)
{
  AGameMode* GameMode = UGameplayStatics::GetGameMode(TheWorld);
  AUE4CookbookGameMode * MyGameMode = Cast<AUE4CookbookGameMode>(GameMode);
  MyGameMode->MyStandardDelegate.ExecuteIfBound();
}
  1. 确保在 CPP 文件中也添加#include "UE4CookbookGameMode.h",以便编译器在使用之前知道该类。

  2. 编译您的游戏。确保您的游戏模式设置在当前级别中(如果您不知道如何设置,请参阅第四章中的使用 SpawnActor 实例化 Actor教程,Actors and Components),并将TriggerVolume的副本拖到级别中。还将DelegateListener的副本拖到级别中,并将其放置在平面表面上方约 100 个单位处:操作步骤...

  3. 当您点击播放,并走进 Trigger volume 覆盖的区域时,您应该看到我们添加到DelegateListenerPointLight组件打开:操作步骤...

工作原理...

  1. 在我们的GameMode头文件中,声明一个不带任何参数的委托类型,称为FTriggerHitSignature

  2. 然后,我们在GameMode类的成员中创建委托的实例。

  3. 我们在DelegateListener中添加一个PointLight组件,以便我们有一个委托被执行的可视表示。

  4. 在构造函数中,我们初始化我们的PointLight,然后禁用它。

  5. 我们重写BeginPlay。我们首先调用父类的BeginPlay()实现。然后,我们获取游戏世界,使用GetGameMode()检索GameMode类。

  6. 将生成的AGameMode*转换为我们的GameMode类的指针需要使用Cast模板函数。

  7. 然后,我们可以访问GameMode的委托实例成员,并将我们的EnableLight函数绑定到委托,这样当委托被执行时就会调用它。

  8. 在这种情况下,我们绑定到UFUNCTION(),所以我们使用BindUObject。如果我们想要绑定到一个普通的 C++类函数,我们将使用BindRaw。如果我们想要绑定到一个静态函数,我们将使用BindStatic()

  9. TriggerVolume与玩家重叠时,它检索GameMode,然后在委托上调用ExecuteIfBound

  10. ExecuteIfBound检查委托是否绑定了函数,然后为我们调用它。

  11. EnableLight函数在被委托对象调用时启用PointLight组件。

另请参阅

  • 接下来的部分,取消委托,向您展示了如何在Listener在委托被调用之前被销毁的情况下安全地取消注册委托绑定

取消委托

有时,有必要移除委托绑定。这就像将函数指针设置为nullptr,这样它就不再引用已被删除的对象。

准备工作

您需要按照先前的教程进行操作,以便您有一个要取消注册的委托。

操作步骤...

  1. DelegateListener中,添加以下重写函数声明:
UFUNCTION()
virtual void EndPlay(constEEndPlayReason::Type EndPlayReason) override;
  1. 实现如下功能:
void ADelegateListener::EndPlay(constEEndPlayReason::Type EndPlayReason)
{
  Super::EndPlay(EndPlayReason);
  UWorld* TheWorld = GetWorld();
  if (TheWorld != nullptr)
  {
    AGameMode* GameMode = UGameplayStatics::GetGameMode(TheWorld);
    AUE4CookbookGameMode * MyGameMode = Cast<AUE4CookbookGameMode>(GameMode);
    if (MyGameMode != nullptr)
    {
      MyGameMode->MyStandardDelegate.Unbind();
    }
  }
}

工作原理...

  1. 本教程将本章迄今为止的两个先前教程结合起来。我们重写EndPlay,这是一个作为虚函数实现的事件,这样我们就可以在DelegateListener离开游戏时执行代码。

  2. 在重写的实现中,我们在委托上调用Unbind()方法,这将从DelegateListener实例中取消链接成员函数。

  3. 如果不这样做,委托就会像指针一样悬空,当DelegateListener离开游戏时,它就处于无效状态。

创建接受输入参数的委托

到目前为止,我们使用的委托没有接受任何输入参数。本教程向您展示如何更改委托的签名,以便它接受一些输入。

准备工作

确保您已经按照本章开头的教程进行了操作,该教程向您展示了如何创建TriggerVolume和我们为本教程所需的其他基础设施。

操作步骤...

  1. GameMode添加一个新的委托声明:
DECLARE_DELEGATE_OneParam(FParamDelegateSignature, FLinearColor)
  1. GameMode添加新成员:
FParamDelegateSignatureMyParameterDelegate;
  1. 创建一个名为ParamDelegateListener的新Actor类。将以下内容添加到声明中:
UFUNCTION()
void SetLightColor(FLinearColorLightColor);
UPROPERTY()
UPointLightComponent* PointLight;
  1. 在类实现中,将以下内容添加到构造函数中:
PointLight = CreateDefaultSubobject<UPointLightComponent>("PointLight");
RootComponent = PointLight;
  1. ParamDelegateListener.cpp文件中,在项目的include文件和ParamDelegateListener头文件之间添加#include "UE4CookbookGameMode.h"。在ParamDelegateListener::BeginPlay实现内部添加以下内容:
Super::BeginPlay();
UWorld* TheWorld = GetWorld();
if (TheWorld != nullptr)
{
  AGameMode* GameMode = UGameplayStatics::GetGameMode(TheWorld);
  AUE4CookbookGameMode * MyGameMode = Cast<AUE4CookbookGameMode>(GameMode);
  if (MyGameMode != nullptr)
  {
    MyGameMode->MyParameterDelegate.BindUObject(this, &AParamDelegateListener::SetLightColor);
  }
}
  1. 最后,实现SetLightColor
void AParamDelegateListener::SetLightColor(FLinearColorLightColor)
{
  PointLight->SetLightColor(LightColor);
}
  1. 在我们的TriggerVolume中,在NotifyActorBeginOverlap中,在调用MyStandardDelegate.ExecuteIfBound之后添加以下行:
MyGameMode->MyParameterDelegate.ExecuteIfBound(FLinearColor(1, 0, 0, 1));

它是如何工作的...

  1. 我们的新委托签名使用了一个稍微不同的宏来声明。请注意DECLARE_DELEGATE_OneParam末尾的_OneParam后缀。正如你所期望的,我们还需要指定参数的类型。

  2. 就像我们创建没有参数的委托时一样,我们需要在我们的GameMode类的成员中创建委托的实例。

  3. 我们现在创建了一个新类型的DelegateListener,它期望将参数传递到绑定到委托的函数中。

  4. 当我们为委托调用ExecuteIfBound()方法时,我们现在需要传入将插入函数参数的值。

  5. 在我们绑定的函数内部,我们使用参数来设置灯光的颜色。

  6. 这意味着TriggerVolume不需要知道任何关于ParamDelegateListener的信息,就可以调用它的函数。委托使我们能够最小化两个类之间的耦合。

另请参阅

  • 取消注册委托食谱向您展示了如何在监听器在调用委托之前被销毁时安全取消注册委托绑定

使用委托绑定传递有效负载数据

只需进行最小的更改,就可以在创建时将参数传递给委托。本食谱向您展示了如何指定要始终作为参数传递给委托调用的数据。这些数据在绑定创建时计算,并且从那时起不会改变。

准备工作

确保您已经按照之前的步骤进行操作。我们将扩展之前的步骤的功能,以将额外的创建时参数传递给我们绑定的委托函数。

如何做...

  1. 在您的AParamDelegateListener::BeginPlay函数内部,将对BindUObject的调用更改为以下内容:
MyGameMode->MyParameterDelegate.BindUObject(this, &AParamDelegateListener::SetLightColor, false);
  1. SetLightColor的声明更改为:
void SetLightColor(FLinearColorLightColor, bool EnableLight);
  1. 修改SetLightColor的实现如下:
void AParamDelegateListener::SetLightColor(FLinearColorLightColor, bool EnableLight)
{
  PointLight->SetLightColor(LightColor);
  PointLight->SetVisibility(EnableLight);
}
  1. 编译并运行您的项目。验证当您走进TriggerVolume时,灯光会关闭,因为在绑定函数时传入了错误的有效负载参数。

它是如何工作的...

  1. 当我们将函数绑定到委托时,我们指定了一些额外的数据(在本例中是一个值为false的布尔值)。您可以以这种方式传递多达四个“有效负载”变量。它们会应用于您的函数,而不是您使用的DECLARE_DELEGATE_*宏中声明的任何参数之后。

  2. 我们更改了委托的函数签名,以便它可以接受额外的参数。

  3. 在函数内部,我们使用额外的参数根据编译时的值是 true 还是 false 来打开或关闭灯光。

  4. 我们不需要更改对ExecuteIfBound的调用 - 委托系统会自动首先应用通过ExecuteIfBound传入的委托参数,然后应用任何有效负载参数,这些参数始终在对BindUObject的调用中函数引用之后指定。

另请参阅

  • 食谱取消注册委托向您展示了如何在监听器在调用委托之前被销毁时安全取消注册委托绑定

创建多播委托

本章迄今为止使用的标准委托本质上是一个函数指针 - 它们允许您在一个特定对象实例上调用一个特定函数。多播委托是一组函数指针,每个指针可能在不同的对象上,当委托被广播时,它们都将被调用。

准备工作

这个示例假设你已经按照本章的初始示例进行了操作,因为它向你展示了如何创建用于广播多播委托的TriggerVolume

如何做...

  1. GameMode头文件中添加新的委托声明:
DECLARE_MULTICAST_DELEGATE(FMulticastDelegateSignature)
  1. 创建一个名为MulticastDelegateListener的新的Actor类。将以下内容添加到声明中:
UFUNCTION()
void ToggleLight();
UFUNCTION()
virtual void EndPlay(constEEndPlayReason::Type EndPlayReason) override;

UPROPERTY()
UPointLightComponent* PointLight;

FDelegateHandleMyDelegateHandle;
  1. 在类实现中,将此添加到构造函数中:
PointLight = CreateDefaultSubobject<UPointLightComponent>("PointLight");
RootComponent = PointLight;
  1. MulticastDelegateListener.cpp文件中,在您项目的include文件和MulticastDelegateListener头文件包含之间添加#include "UE4CookbookGameMode.h"。在MulticastDelegateListener::BeginPlay实现中,添加以下内容:
Super::BeginPlay();
UWorld* TheWorld = GetWorld();
if (TheWorld != nullptr)
{
  AGameMode* GameMode = UGameplayStatics::GetGameMode(TheWorld);
  AUE4CookbookGameMode * MyGameMode = Cast<AUE4CookbookGameMode>(GameMode);
  if (MyGameMode != nullptr)
  {
    MyDelegateHandle  = MyGameMode->MyMulticastDelegate.AddUObject(this, &AMulticastDelegateListener::ToggleLight);
  }
}
  1. 实现ToggleLight
void AMulticastDelegateListener::ToggleLight()
{
  PointLight->ToggleVisibility();
}
  1. 实现我们的EndPlay重写函数:
void AMulticastDelegateListener::EndPlay(constEEndPlayReason::Type EndPlayReason)
{
  Super::EndPlay(EndPlayReason);
  UWorld* TheWorld = GetWorld();
  if (TheWorld != nullptr)
  {
    AGameMode* GameMode = UGameplayStatics::GetGameMode(TheWorld);
    AUE4CookbookGameMode * MyGameMode = Cast<AUE4CookbookGameMode>(GameMode);
    if (MyGameMode != nullptr)
    {
      MyGameMode->MyMulticastDelegate.Remove(MyDelegateHandle);
    }
  }
}
  1. TriggerVolume::NotifyActorBeginOverlap()中添加以下行:
MyGameMode->MyMulticastDelegate.Broadcast();
  1. 编译并加载您的项目。将您的级别中的GameMode设置为我们的烹饪书游戏模式,然后将四到五个MulticastDelegateListener的实例拖到场景中。

  2. 步入TriggerVolume以查看所有MulticastDelegateListener切换其灯光的可见性。

工作原理...

  1. 正如你所期望的那样,委托类型需要明确声明为多播委托,而不是标准的单绑定委托。

  2. 我们的新Listener类与我们原始的DelegateListener非常相似。主要区别在于,我们需要在FDelegateHandle中存储对委托实例的引用。

  3. 当演员被销毁时,我们可以使用存储的FDelegateHandle作为Remove()的参数,安全地将自己从绑定到委托的函数列表中移除。

  4. Broadcast()函数是ExecuteIfBound()的多播等效。与标准委托不同,无需提前检查委托是否绑定,也不需要像ExecuteIfBound一样调用。无论绑定了多少个函数,甚至没有绑定任何函数,Broadcast()都是安全运行的。

  5. 当我们在场景中有多个多播监听器实例时,它们将分别向在GameMode中实现的多播委托注册自己。

  6. 然后,当TriggerVolume与玩家重叠时,它会广播委托,每个监听器都会收到通知,导致它们切换其关联点光的可见性。

  7. 多播委托可以以与标准委托完全相同的方式接受参数。

创建自定义事件

自定义委托非常有用,但它们的一个限制是它们可以被一些其他第三方类外部广播,也就是说,它们的 Execute/Broadcast 方法是公开可访问的。

有时,您可能需要一个委托,可以由其他类外部分配,但只能由包含它们的类广播。这是事件的主要目的。

准备工作

确保您已经按照本章的初始示例进行了操作,以便您拥有MyTriggerVolumeCookBookGameMode的实现。

如何做...

  1. 将以下事件声明宏添加到您的MyTriggerVolume类的头文件中:
DECLARE_EVENT(AMyTriggerVolume, FPlayerEntered)
  1. 向类添加已声明事件签名的实例:
FPlayerEnteredOnPlayerEntered;
  1. AMyTriggerVolume::NotifyActorBeginOverlap中添加此内容:
OnPlayerEntered.Broadcast();
  1. 创建一个名为TriggerVolEventListener的新的Actor类。

  2. 向其声明中添加以下类成员:

UPROPERTY()
UPointLightComponent* PointLight;

UPROPERTY(EditAnywhere)
AMyTriggerVolume* TriggerEventSource;
UFUNCTION()
void OnTriggerEvent();
  1. 在类构造函数中初始化PointLight
PointLight = CreateDefaultSubobject<UPointLightComponent>("PointLight");
RootComponent = PointLight;
  1. BeginPlay中添加以下内容:
if (TriggerEventSource != nullptr)
{
  TriggerEventSource->OnPlayerEntered.AddUObject(this, &ATriggerVolEventListener::OnTriggerEvent);
}
  1. 最后,实现OnTriggerEvent()
void ATriggerVolEventListener::OnTriggerEvent()
{
  PointLight->SetLightColor(FLinearColor(0, 1, 0, 1));
}
  1. 编译您的项目,并启动编辑器。创建一个级别,其中游戏模式设置为我们的UE4CookbookGameMode,然后将ATriggerVolEventListenerAMyTriggerVolume的一个实例拖到级别中。

  2. 选择TriggerVolEventListener,您将在详细信息面板中的类别中看到TriggerVolEventListener列出,其中包含属性Trigger Event Source如何做...

  3. 使用下拉菜单选择您的AMyTriggerVolume实例,以便监听器知道要绑定到哪个事件:如何做...

  4. 玩游戏,并进入触发体积的影响区域。验证您的EventListener的颜色是否变为绿色。

它是如何工作的...

  1. 与所有其他类型的代表一样,事件需要它们自己的特殊宏函数。

  2. 第一个参数是事件将被实现到的类。这将是唯一能够调用Broadcast()的类,所以确保它是正确的类。

  3. 第二个参数是我们新事件函数签名的类型名称。

  4. 我们在我们的类中添加了这种类型的实例。虚幻文档建议使用On<x>作为命名惯例。

  5. 当某物与我们的TriggerVolume重叠时,我们调用我们自己事件实例的广播。

  6. 在新类中,我们创建一个点光源作为事件被触发的可视表示。

  7. 我们还创建了一个指向TriggerVolume的指针来监听事件。我们将UPROPERTY标记为EditAnywhere,因为这样可以在编辑器中设置它,而不必使用GetAllActorsOfClass或其他方式在程序中获取引用。

  8. 最后是我们的事件处理程序,当某物进入TriggerVolume时。

  9. 我们像往常一样在构造函数中创建和初始化我们的点光源。

  10. 游戏开始时,监听器检查我们的TriggerVolume引用是否有效,然后将我们的OnTriggerEvent函数绑定到TriggerVolume事件。

  11. OnTriggerEvent中,我们将灯光的颜色改为绿色。

  12. 当某物进入TriggerVolume时,它会导致TriggerVolume调用自己的事件广播。然后我们的TriggerVolEventListener就会调用其绑定的方法,改变我们灯光的颜色。

创建一个时间处理程序

这个教程向您展示了如何使用前面介绍的概念来创建一个演员,它通知其他演员游戏内时间的流逝。

如何做...

  1. 创建一个名为TimeOfDayHandler的新的Actor类。

  2. 在头文件中添加一个多播代表声明:

DECLARE_MULTICAST_DELEGATE_TwoParams(FOnTimeChangedSignature, int32, int32)
  1. 将我们的代表的一个实例添加到类声明中:
FOnTimeChangedSignatureOnTimeChanged;
  1. 将以下属性添加到类中:
UPROPERTY()
int32 TimeScale;

UPROPERTY()
int32 Hours;
UPROPERTY()
int32 Minutes;

UPROPERTY()
float ElapsedSeconds;
  1. 将这些属性的初始化添加到构造函数中:
TimeScale = 60;
Hours = 0;
Minutes = 0;
ElapsedSeconds = 0;
  1. Tick中,添加以下代码:
ElapsedSeconds += (DeltaTime * TimeScale);
if (ElapsedSeconds> 60)
{
  ElapsedSeconds -= 60;
  Minutes++;
  if (Minutes > 60)
  {
    Minutes -= 60;
    Hours++;
  }

  OnTimeChanged.Broadcast(Hours, Minutes);
}
  1. 创建一个名为Clock的新的Actor类。

  2. 将以下属性添加到类头部:

UPROPERTY()
USceneComponent* RootSceneComponent;

UPROPERTY()
UStaticMeshComponent* ClockFace;
UPROPERTY()
USceneComponent* HourHandle;
UPROPERTY()
UStaticMeshComponent* HourHand;
UPROPERTY()
USceneComponent* MinuteHandle;
UPROPERTY()
UStaticMeshComponent* MinuteHand;

UFUNCTION()
void TimeChanged(int32 Hours, int32 Minutes);
FDelegateHandleMyDelegateHandle;
  1. 在构造函数中初始化和转换组件:
RootSceneComponent = CreateDefaultSubobject<USceneComponent>("RootSceneComponent");
ClockFace = CreateDefaultSubobject<UStaticMeshComponent>("ClockFace");
HourHand = CreateDefaultSubobject<UStaticMeshComponent>("HourHand");
MinuteHand = CreateDefaultSubobject<UStaticMeshComponent>("MinuteHand");
HourHandle = CreateDefaultSubobject<USceneComponent>("HourHandle");
MinuteHandle = CreateDefaultSubobject<USceneComponent>("MinuteHandle");
auto MeshAsset = ConstructorHelpers::FObjectFinder<UStaticMesh>(TEXT("StaticMesh'/Engine/BasicShapes/Cylinder.Cylinder'"));
if (MeshAsset.Object != nullptr)
{
  ClockFace->SetStaticMesh(MeshAsset.Object);
  HourHand->SetStaticMesh(MeshAsset.Object);
  MinuteHand->SetStaticMesh(MeshAsset.Object);
}
RootComponent = RootSceneComponent;
HourHand->AttachTo(HourHandle);
MinuteHand->AttachTo(MinuteHandle);
HourHandle->AttachTo(RootSceneComponent);
MinuteHandle->AttachTo(RootSceneComponent);
ClockFace->AttachTo(RootSceneComponent);
ClockFace->SetRelativeTransform(FTransform(FRotator(90, 0, 0), FVector(10, 0, 0), FVector(2, 2, 0.1)));
HourHand->SetRelativeTransform(FTransform(FRotator(0, 0, 0), FVector(0, 0, 25), FVector(0.1, 0.1, 0.5)));
MinuteHand->SetRelativeTransform(FTransform(FRotator(0, 0, 0), FVector(0, 0, 50), FVector(0.1, 0.1, 1)));
  1. 将以下内容添加到BeginPlay中:
TArray<AActor*>TimeOfDayHandlers;
UGameplayStatics::GetAllActorsOfClass(GetWorld(), ATimeOfDayHandler::StaticClass(), TimeOfDayHandlers);
if (TimeOfDayHandlers.Num() != 0)
{
  auto TimeOfDayHandler = Cast<ATimeOfDayHandler>(TimeOfDayHandlers[0]);
  MyDelegateHandle = TimeOfDayHandler->OnTimeChanged.AddUObject(this, &AClock::TimeChanged);
}
  1. 最后,实现TimeChanged作为您的事件处理程序。
void AClock::TimeChanged(int32 Hours, int32 Minutes)
{
  HourHandle->SetRelativeRotation(FRotator( 0, 0,30 * Hours));
  MinuteHandle->SetRelativeRotation(FRotator(0,0,6 * Minutes));
}
  1. 在您的级别中放置一个TimeOfDayHandlerAClock的实例,并播放以查看时钟上的指针是否在旋转:如何做...

它是如何工作的...

  1. TimeOfDayHandler包含一个带有两个参数的代表,因此使用宏的TwoParams变体。

  2. 我们的类包含变量来存储小时、分钟和秒,以及TimeScale,这是一个用于加速测试目的的加速因子。

  3. 在处理程序的Tick函数中,我们根据自上一帧以来经过的时间累积经过的秒数。

  4. 我们检查经过的秒数是否超过了 60。如果是,我们减去 60,并增加Minutes

  5. 同样,对于Minutes——如果它们超过 60,我们减去 60,并增加Hours

  6. 如果MinutesHours被更新,我们会广播我们的代表,让订阅了代表的任何对象都知道时间已经改变。

  7. Clock actor 使用一系列场景组件和静态网格来构建类似时钟表盘的网格层次结构。

  8. Clock构造函数中,我们将层次结构中的组件进行父子关联,并设置它们的初始比例和旋转。

  9. BeginPlay中,时钟使用GetAllActorsOfClass()来获取级别中所有的time of day处理程序。

  10. 如果级别中至少有一个TimeOfDayHandlerClock就会访问第一个,并订阅其TimeChanged事件。

  11. TimeChanged事件触发时,时钟会根据当前时间的小时和分钟数旋转时针和分针。

为第一人称射击游戏创建一个重生拾取物

这个教程向您展示了如何创建一个可放置的拾取物,在一定时间后重新生成,适用于 FPS 中的弹药或其他拾取物。

如何做...

  1. 创建一个名为Pickup的新的Actor类。

  2. Pickup.h中声明以下委托类型:

DECLARE_DELEGATE(FPickedupEventSignature)
  1. 将以下属性添加到类头文件中:
virtual void NotifyActorBeginOverlap(AActor* OtherActor) override;
UPROPERTY()
UStaticMeshComponent* MyMesh;

UPROPERTY()
URotatingMovementComponent* RotatingComponent;

FPickedupEventSignatureOnPickedUp;
  1. 将以下代码添加到构造函数中:
MyMesh = CreateDefaultSubobject<UStaticMeshComponent>("MyMesh");
RotatingComponent = CreateDefaultSubobject<URotatingMovementComponent>("RotatingComponent");
RootComponent = MyMesh;
auto MeshAsset = ConstructorHelpers::FObjectFinder<UStaticMesh>(TEXT("StaticMesh'/Engine/BasicShapes/Cube.Cube'"));
if (MeshAsset.Object != nullptr)
{
  MyMesh->SetStaticMesh(MeshAsset.Object);
}
MyMesh->SetCollisionProfileName(TEXT("OverlapAllDynamic"));
RotatingComponent->RotationRate = FRotator(10, 0, 10);
  1. 实现重写的NotifyActorBeginOverlap
void APickup::NotifyActorBeginOverlap(AActor* OtherActor)
{
  OnPickedUp.ExecuteIfBound();
}
  1. 创建第二个名为PickupSpawnerActor类。

  2. 将以下内容添加到类头文件中:

UPROPERTY()
USceneComponent* SpawnLocation;

UFUNCTION()
void PickupCollected();
UFUNCTION()
void SpawnPickup();
UPROPERTY()
APickup* CurrentPickup;
FTimerHandleMyTimer;
  1. PickupSpawner的实现文件中将Pickup.h添加到包含文件中。

  2. 在构造函数中初始化我们的根组件:

SpawnLocation = CreateDefaultSubobject<USceneComponent>("SpawnLocation");
  1. BeginPlay中使用SpawnPickup函数在游戏开始时生成一个拾取物:
SpawnPickup();
  1. 实现PickupCollected
void APickupSpawner::PickupCollected()
{
  GetWorld()->GetTimerManager().SetTimer(MyTimer, this, &APickupSpawner::SpawnPickup, 10, false);
  CurrentPickup->OnPickedUp.Unbind();
  CurrentPickup->Destroy();
}
  1. SpawnPickup创建以下代码:
void APickupSpawner::SpawnPickup()
{
  UWorld* MyWorld = GetWorld();
  if (MyWorld != nullptr){
    CurrentPickup = MyWorld->SpawnActor<APickup>(APickup::StaticClass(), GetTransform());
    CurrentPickup->OnPickedUp.BindUObject(this, &APickupSpawner::PickupCollected);
  }
}
  1. 编译并启动编辑器,然后将PickupSpawner的一个实例拖到关卡中。走到由旋转立方体表示的拾取物上,并验证它在 10 秒后再次生成:如何做...

工作原理...

  1. 像往常一样,在我们的Pickup内部创建一个委托,以便我们的 Spawner 可以订阅它,以便它知道玩家何时收集了拾取物。

  2. Pickup还包含一个静态网格作为视觉表示,以及一个RotatingMovementComponent,使网格以一种方式旋转,以吸引玩家的注意。

  3. Pickup构造函数中,我们加载引擎内置的网格作为我们的视觉表示。

  4. 我们指定网格将与其他对象重叠,然后在XZ轴上将网格的旋转速率设置为每秒 10 个单位。

  5. 当玩家与Pickup重叠时,它会从第一步触发其PickedUp委托。

  6. PickupSpawner有一个场景组件来指定生成拾取物的位置。它有一个执行此操作的函数,并且有一个UPROPERTY标记的对当前生成的Pickup的引用。

  7. PickupSpawner构造函数中,我们像往常一样初始化我们的组件。

  8. 游戏开始时,Spawner 运行其SpawnPickup函数。

  9. 这个函数生成我们的Pickup的一个实例,然后将APickupSpawner::PickupCollected绑定到新实例上的OnPickedUp函数。它还存储对当前实例的引用。

  10. 当玩家与Pickup重叠后,PickupCollected运行,创建一个定时器在 10 秒后重新生成拾取物。

  11. 移除到已收集拾取物的现有委托绑定,然后销毁拾取物。

  12. 10 秒后,定时器触发,再次运行SpawnActor,创建一个新的Pickup

第六章:输入和碰撞

本章涵盖了围绕游戏控制输入(键盘、鼠标和游戏手柄)以及与障碍物的碰撞相关的教程。

本章将涵盖以下教程:

  • 轴映射-键盘、鼠标和游戏手柄方向输入,用于 FPS 角色

  • 轴映射-标准化输入

  • 动作映射-用于 FPS 角色的单按钮响应

  • 从 C++添加轴和动作映射

  • 鼠标 UI 输入处理

  • UMG 键盘 UI 快捷键

  • 碰撞-使用忽略让物体相互穿过

  • 碰撞-使用重叠拾取物体

  • 碰撞-使用阻止防止相互穿透

介绍

良好的输入控件在您的游戏中非常重要。提供键盘、鼠标和尤其是游戏手柄输入将使您的游戏更受用户欢迎。

提示

介绍 您可以在 Windows PC 上使用 Xbox 360 和 PlayStation 控制器-它们具有 USB 输入。检查您当地的电子商店,以找到一些好的 USB 游戏控制器。您还可以使用无线控制器,连接到 PC 的游戏控制器无线接收器适配器。

轴映射-键盘、鼠标和游戏手柄方向输入,用于 FPS 角色

有两种类型的输入映射:轴映射动作映射。轴映射是您按住一段时间以获得其效果的输入(例如,按住W键移动玩家向前),而动作映射是一次性输入(例如,按下游戏手柄上的A键使玩家跳跃)。在本教程中,我们将介绍如何设置键盘、鼠标和游戏手柄轴映射输入控件以移动 FPS 角色。

准备就绪

您必须有一个 UE4 项目,其中有一个主角玩家,以及一个地面平面可供行走,以准备进行此操作。

如何做...

  1. 创建一个 C++类,Warrior,从Character派生:
UCLASS()
class CH6_API AWarrior : public ACharacter
{
  GENERATED_BODY()
};
  1. 启动 UE4,并根据您的Warrior类派生一个蓝图,BP_Warrior

  2. 创建并选择一个新的GameMode类蓝图,如下所示:

  3. 转到设置 | 项目设置 | 地图和模式

  4. 单击默认GameMode下拉菜单旁边的+图标,这将创建一个GameMode类的新蓝图,并选择您选择的名称(例如BP_GameMode)。

  5. 双击您创建的新BP_GameMode蓝图类以进行编辑。

  6. 打开您的BP_GameMode蓝图,并选择您的蓝图化的BP_Warrior类作为默认的Pawn类。

  7. 要设置键盘输入驱动玩家,打开设置 | 项目设置 | 输入。在接下来的步骤中,我们将完成在游戏中驱动玩家向前的过程:

  8. 单击轴映射标题旁边的+图标。

提示

轴映射支持连续(按住按钮)输入,而动作映射支持一次性事件。

  1. 为轴映射命名。第一个示例将展示如何移动玩家向前,因此将其命名为Forward

  2. Forward下方,选择一个键盘键来分配给此轴映射,例如W

  3. 单击Forward旁边的+图标,并选择一个游戏控制器输入,以将玩家前进映射到移动玩家的游戏控制器左拇指杆上。

  4. 使用键盘、游戏手柄和可选的鼠标输入绑定,完成轴映射的后退、左转和右转。

  5. 从您的 C++代码中,重写AWarrior类的SetupPlayerInputComponent函数,如下所示:

void AWarrior::SetupPlayerInputComponent(UInputComponent* Input)
{
  check(Input);
  Input->BindAxis( "Forward", this, &AWarrior::Forward );
}
  1. 在您的AWarrior类中提供一个Forward函数,如下所示:
void AWarrior::Forward( float amount )
{
  if( Controller && amount )
  {
    // Moves the player forward by an amount in forward direction
    AddMovementInput(GetActorForwardVector(), amount );
  }
}
  1. 编写并完成其余输入方向的函数,AWarrior::BackAWarrior::LeftAWarrior::Right

它是如何工作的...

UE4 引擎允许直接将输入事件连接到 C++函数调用。由输入事件调用的函数是某个类的成员函数。在前面的示例中,我们将W键的按下和手柄的左摇杆向上按下都路由到了AWarrior::Forward的 C++函数。调用AWarrior::Forward的实例是路由控制器输入的实例。这由在GameMode类中设置为玩家角色的对象控制。

另请参阅

  • 您可以实际上从 C++中编写Forward输入轴绑定,而不是在 UE4 编辑器中输入。我们将在以后的示例中详细描述这一点,从 C++添加轴和动作映射

轴映射 - 规范化输入

如果您注意到,右侧和前方的输入为 1.0 实际上会总和为 2.0 的速度。这意味着在对角线上移动可能比纯粹向前、向后、向左或向右移动更快。我们真正应该做的是夹住任何导致速度超过 1.0 单位的输入值,同时保持指示的输入方向。我们可以通过存储先前的输入值并覆盖::Tick()函数来实现这一点。

准备工作

打开一个项目,并设置一个Character派生类(我们称之为Warrior)。

如何做…

  1. 如下覆盖AWarrior::SetupPlayerInputComponent( UInputComponent* Input )函数:
void AWarrior::SetupPlayerInputComponent( UInputComponent* Input )
{
  Input->BindAxis( "Forward", this, &AWarrior::Forward );
  Input->BindAxis( "Back", this, &AWarrior::Back );
  Input->BindAxis( "Right", this, &AWarrior::Right );
  Input->BindAxis( "Left", this, &AWarrior::Left );
}
  1. 编写相应的::Forward::Back::Right::Left函数如下:
void AWarrior::Forward( float amount ) {
  // We use a += of the amount added so that
  // when the other function modifying .Y
  // (::Back()) affects lastInput, it won't
  // overwrite with 0's
  lastInput.Y += amount;
}
void AWarrior::Back( float amount ) {
  lastInput.Y += -amount;
}
void AWarrior::Right( float amount ) {
  lastInput.X += amount;
}
void AWarrior::Left( float amount ) {
  lastInput.X += -amount;
}
  1. AWarrior::Tick()函数中,在规范化输入向量中任何超大值后修改输入值:
void AWarrior::Tick( float DeltaTime ) {
  Super::Tick( DeltaTime );
  if( Controller )
  {
    float len = lastInput.Size();
    if( len > 1.f )
      lastInput /= len;
    AddMovementInput(
    GetActorForwardVector(), lastInput.Y );
    AddMovementInput(GetActorRightVector(), lastInput.X);
    // Zero off last input values
    lastInput = FVector2D( 0.f, 0.f );
  }
}

工作原理…

当输入向量超过 1.0 的幅度时,我们对其进行规范化。这将限制最大输入速度为 1.0 单位(例如,当完全向上和向右按下时,速度为 2.0 单位)。

动作映射 - 用于 FPS 角色的单按钮响应

动作映射用于处理单按钮按下(而不是按住的按钮)。对于应该按住的按钮,请确保使用轴映射。

准备工作

准备好一个带有您需要完成的操作的 UE4 项目,例如JumpShootGun

如何做…

  1. 打开设置 | 项目设置 | 输入

  2. 转到动作映射标题,并单击旁边的+图标。

  3. 开始输入应映射到按钮按下的操作。例如,为第一个动作输入Jump

  4. 选择要按下的键以执行该操作,例如,空格键

  5. 如果您希望通过另一个按键触发相同的操作,请单击动作映射名称旁边的+,然后选择另一个按键来触发该操作。

  6. 如果要求ShiftCtrlAltCmd键必须按下才能触发操作,请确保在键选择框右侧的复选框中指示。

如何做…

  1. 要将您的操作链接到 C++代码函数,您需要覆盖SetupPlayerInputComponent(UInputControl* control )函数。在该函数内输入以下代码:
voidAWarrior::SetupPlayerInputComponent(UInputComponent* Input)
{
  check(Input );
  // Connect the Jump action to the C++ Jump function
  Input->BindAction("Jump", IE_Pressed, this, &AWarrior::Jump );
}

工作原理…

动作映射是单按钮按下事件,触发 C++代码以响应它们运行。您可以在 UE4 编辑器中定义任意数量的操作,但请确保将动作映射与 C++中的实际按键绑定起来。

另请参阅

  • 您可以列出您希望从 C++代码映射的操作。有关此信息,请参阅从 C++添加轴和动作映射中的以下示例。

从 C++添加轴和动作映射

轴映射动作映射可以通过 UE4 编辑器添加到游戏中,但我们也可以直接从 C++代码中添加它们。由于 C++函数的连接本来就是从 C++代码进行的,因此您可能会发现在 C++中定义您的轴和动作映射也很方便。

准备工作

您需要一个 UE4 项目,您想要在其中添加一些轴和动作映射。如果您通过 C++代码添加它们,您可以删除Settings | Project Settings | Input中列出的现有轴和动作映射。要添加您的自定义轴和动作映射,有两个 C++函数您需要了解:UPlayerInput::AddAxisMappingUPlayerInput::AddActionMapping。这些是UPlayerInput对象上可用的成员函数。UPlayerInput对象位于PlayerController对象内,可以通过以下代码访问:

GetWorld()->GetFirstPlayerController()->PlayerInput

您还可以使用UPlayerInput的两个静态成员函数来创建您的轴和动作映射,如果您不想单独访问玩家控制器的话:

UPlayerInput::AddEngineDefinedAxisMapping()
UPlayerInput::AddEngineDefinedActionMapping()

如何做...

  1. 首先,我们需要定义我们的FInputAxisKeyMappingFInputActionKeyMapping对象,具体取决于您是连接轴键映射(用于按下按钮进行输入)还是连接动作键映射(用于一次性事件-按下按钮进行输入)。

  2. 对于轴键映射,我们定义一个FInputAxisKeyMapping对象,如下所示:

FInputAxisKeyMapping backKey( "Back", EKeys::S, 1.f );
  1. 这将包括动作的字符串名称,要按的键(使用 EKeys enum),以及是否应按住ShiftCtrlAltcmd(Mac)来触发事件。

  2. 对于动作键映射,定义FInputActionKeyMapping,如下所示:

FInputActionKeyMapping jump("Jump", EKeys::SpaceBar, 0, 0, 0, 0);
  1. 这将包括动作的字符串名称,要按的键,以及是否应按住ShiftCtrlAltcmd(Mac)来触发事件。

  2. 在您的玩家Pawn类的SetupPlayerInputComponent函数中,将您的轴和动作键映射注册到以下内容:

  3. 与特定控制器连接的PlayerInput对象:

GetWorld()->GetFirstPlayerController()->PlayerInput->AddAxisMapping( backKey ); // specific to a controller
  1. 或者,您可以直接注册到UPlayerInput对象的静态成员函数:
UPlayerInput::AddEngineDefinedActionMapping(jump );

提示

确保您对轴与动作映射使用了正确的函数!

  1. 使用 C++代码注册您的动作和轴映射到 C++函数,就像前两个示例中所示的那样:
Input->BindAxis("Back", this, &AWarrior::Back);
Input->BindAction("Jump", IE_Pressed, this, &AWarrior::Jump );

它是如何工作的...

动作和轴映射注册函数允许您直接从 C++代码设置您的输入映射。C++编码的输入映射本质上与在Settings | Project Settings | Input对话框中输入映射相同。

鼠标 UI 输入处理

在使用虚幻运动图形(UMG)工具包时,您会发现鼠标事件非常容易处理。我们可以注册 C++函数以在鼠标单击或与 UMG 组件的其他类型交互后运行。

通常,事件注册将通过蓝图进行;但在这个示例中,我们将概述如何编写和连接 UMG 事件的 C++函数。

准备工作

在您的 UE4 项目中创建一个 UMG 画布。从那里,我们将为OnClickedOnPressedOnReleased事件注册事件处理程序。

如何做...

  1. Content Browser中右键单击(或单击Add New),然后选择User Interface | Widget Blueprint,如下截图所示。这将向您的项目添加一个可编辑的小部件蓝图。How to do it...

  2. 双击您的Widget Blueprint进行编辑。

  3. 通过从左侧的调色板拖动按钮来向界面添加按钮。

  4. 滚动Details面板,直到找到Events子部分。

  5. 单击您想要处理的任何事件旁边的+图标。How to do it...

  6. 将出现在蓝图中的事件连接到任何具有BlueprintCallable标签的 C++ UFUNCTION()。例如,在您的GameMode类派生中,您可以包括一个函数,如下:

UFUNCTION(BlueprintCallable, Category = UIFuncs)
void ButtonClicked()
{
  UE_LOG(LogTemp, Warning, TEXT( "UI Button Clicked" ) );
}
  1. 通过在您选择的事件下的蓝图图表中路由到它来触发函数调用。

  2. 通过在GameModeBegin Play函数中调用Create Widget,然后调用Add to Viewport来构建和显示您的 UI(或任何主要对象)。

它是如何工作的...

您的小部件蓝图的按钮事件可以轻松连接到蓝图事件,或通过前面的方法连接到 C++函数。

UMG 键盘 UI 快捷键

每个用户界面都需要与之关联的快捷键。要将这些程序到您的 UMG 界面中,您可以简单地将某些键组合连接到一个动作映射中。当动作触发时,只需调用与 UI 按钮本身触发相同的蓝图函数。

准备工作

您应该已经创建了一个 UMG 界面,就像前面的示例中所示的那样。

如何做…

  1. 设置 | 项目设置 | 输入中,为您的热键事件定义一个新的动作映射,例如HotKey_UIButton_Spell

  2. 将事件连接到您的 UI 的函数调用,无论是在蓝图中还是在 C++代码中。

工作原理…

通过将动作映射与 UI 调用的函数进行短路连接,可以使您在游戏程序中很好地实现热键。

碰撞 - 使用忽略让物体相互穿过

碰撞设置相当容易获得。碰撞有三类交集:

  • 忽略:相互穿过而没有任何通知的碰撞。

  • 重叠:触发OnBeginOverlapOnEndOverlap事件的碰撞。允许具有重叠设置的对象相互渗透。

  • 阻止:阻止所有相互渗透的碰撞,并完全阻止物体相互重叠。

对象被归类为许多对象类型之一。特定蓝图组件的碰撞设置允许您将对象归类为您选择的对象类型,并指定该对象如何与所有其他类型的所有其他对象发生碰撞。这在蓝图编辑器的详细信息 | 碰撞部分以表格格式呈现。

例如,以下屏幕截图显示了角色的CapsuleComponent碰撞设置:

碰撞 - 使用忽略让物体相互穿过

准备工作

您应该有一个 UE4 项目,其中包含一些您希望为其编程交集的对象。

如何做…

  1. 打开蓝图编辑器,选择您希望其他对象只是穿过并忽略的对象。在组件列表下,选择您想要设置程序的组件。

  2. 选择您的组件后,查看您的详细信息标签(通常在右侧)。在碰撞预设下,选择无碰撞自定义…预设。

  3. 如果选择无碰撞预设,您可以只需保持不变,所有碰撞都将被忽略。

  4. 如果选择自定义…预设,则选择以下之一:

  5. 无碰撞启用碰撞下拉菜单中。

  6. 启用碰撞下选择一个碰撞模式,确保为每个您希望忽略碰撞的对象类型勾选忽略复选框。

工作原理…

忽略的碰撞不会触发任何事件,也不会阻止标记为忽略的对象之间的相互渗透。

碰撞 - 使用重叠拾取物品

物品拾取是一件非常重要的事情。在这个示例中,我们将概述如何使用 Actor 组件基元上的重叠事件使物品拾取起作用。

准备工作

前面的示例,碰撞:使用忽略让物体相互穿过,描述了碰撞的基础知识。在开始本示例之前,您应该阅读它以了解背景。我们将在这里创建一个新对象通道…来识别Item类对象,以便可以对其进行重叠的编程,只与玩家角色的碰撞体发生重叠。

如何做…

  1. 首先为Item对象的碰撞基元创建一个独特的碰撞通道。在项目设置 | 碰撞下,通过转到新对象通道…来创建一个新的对象通道!如何做…

  2. 将新的对象通道命名为Item

  3. 取你的Item角色并选择用于与玩家角色交叉拾取的基本组件。将该基本组件的对象类型设置为Item类的对象类型

  4. 勾选Pawn对象类型旁边的Overlap复选框,如下截图所示:操作步骤...

  5. 确保勾选Generate Overlap Events复选框。操作步骤...

  6. 选择将拾取物品的玩家角色,并选择他身上用于寻找物品的组件。通常,这将是他的CapsuleComponent。检查与Item对象的Overlap操作步骤...

  7. 现在玩家重叠了物品,物品也重叠了玩家角色。我们必须双向信号重叠(Item重叠PawnPawn重叠Item)才能正常工作。确保Pawn交叉组件的Generate Overlap Events也被勾选。

  8. 接下来,我们必须完成OnComponentBeginOverlap事件,要么是对物品,要么是对玩家的拾取体积,使用蓝图或 C++代码。

  9. 如果你更喜欢蓝图,在 Coin 的可交叉组件的Details面板的Events部分,点击On Component Begin Overlap事件旁边的+图标。操作步骤...

  10. 使用出现在你的Actor蓝图图表中的OnComponentBeginOverlap事件,将蓝图代码连接到玩家的胶囊体积发生重叠时运行。

  11. 如果你更喜欢 C++,你可以编写并附加一个 C++函数到CapsuleComponent。在你的玩家角色类中编写一个成员函数,签名如下:

UFUNCTION(BlueprintNativeEvent, Category = Collision)
void OnOverlapsBegin( UPrimitiveComponent* Comp, AActor* OtherActor,
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex,
bool bFromSweep, const FHitResult& SweepResult );

提示

在 UE 4.13 中,OnOverlapsBegin 函数的签名已更改为:

OnOverlapsBegin( UPrimitiveComponent* Comp, AActor* OtherActor,UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitREsult& SweepResult );
  1. 在你的.cpp文件中完成OnOverlapsBegin()函数的实现,确保以_Implementation结束函数名:
void AWarrior::OnOverlapsBegin_Implementation( AActor*
OtherActor, UPrimitiveComponent* OtherComp,
int32 OtherBodyIndex,
bool bFromSweep, const FHitResult& SweepResult )
{
  UE_LOG(LogTemp, Warning, TEXT( "Overlaps began" ) );
}
  1. 然后,提供一个PostInitializeComponents()覆盖,将OnOverlapsBegin()函数与你的角色类中的胶囊体重叠连接起来,如下所示:
void AWarrior::PostInitializeComponents()
{
  Super::PostInitializeComponents();
  if(RootComponent )
  {
    // Attach contact function to all bounding components.
    GetCapsuleComponent()->OnComponentBeginOverlap.AddDynamic( this, &AWarrior::OnOverlapsBegin );
    GetCapsuleComponent()->OnComponentEndOverlap.AddDynamic( this, &AWarrior::OnOverlapsEnd );
  }
}

它是如何工作的...

引擎引发的Overlap事件允许代码在两个 UE4Actor组件重叠时运行,而不会阻止对象的相互穿透。

碰撞 - 使用阻挡来防止穿透

阻挡意味着在引擎中将阻止Actor组件相互穿透,并且在发现碰撞后,任何两个基本形状之间的碰撞将被解决,不会重叠。

准备工作

从一个具有附加到它们的碰撞基元的对象的 UE4 项目开始(SphereComponentsCapsuleComponentsBoxComponents)。

如何做...

  1. 打开你想要阻挡另一个角色的角色的蓝图。例如,我们希望玩家角色阻挡其他玩家角色实例。

  2. Details面板中标记你不希望与其他组件相互穿透的角色内的基元,将这些组件标记为Blocking操作步骤...

它是如何工作的...

当对象相互阻挡时,它们将不被允许相互穿透。任何穿透将被自动解决,并且对象将被推开。

还有更多...

你可以重写OnComponentHit函数,以便在两个对象相撞时运行代码。这与OnComponentBeginOverlap事件是不同的。

第七章:类和接口之间的通信

本章向您展示如何编写自己的 UInterfaces,并演示如何在 C++中利用它们来最小化类耦合并帮助保持代码清晰。本章将涵盖以下内容:

  • 创建一个UInterface

  • 在对象上实现UInterface

  • 检查类是否实现了UInterface

  • 在本地代码中实现UInterface的转换

  • 从 C++调用本地UInterface函数

  • 相互继承UInterface

  • 在 C++中重写UInterface函数

  • 从本地基类向蓝图公开UInterface方法

  • 在蓝图中实现UInterface函数

  • 创建 C++ UInterface函数实现,可以在蓝图中重写

  • 从 C++调用蓝图定义的接口函数

  • 使用 UInterfaces 实现简单的交互系统

介绍

在您的游戏项目中,有时需要一系列潜在不同的对象共享共同的功能,但使用继承是不合适的,因为这些不同对象之间没有“是一个”关系。诸如 C++的语言倾向于使用多重继承来解决这个问题。

然而,在虚幻中,如果您希望从父类中的函数都可以在蓝图中访问,您需要将它们都设置为UCLASS。这有两个问题。在同一个对象中两次继承UClass会破坏UObject应该形成一个整洁的可遍历层次结构的概念。这也意味着对象上有两个UClass方法的实例,并且它们在代码中必须明确区分。虚幻代码库通过从 C#借用一个概念来解决这个问题——显式接口类型。

使用这种方法的原因是,与组合相比,组件只能在 Actor 上使用,而不能在一般的 UObjects 上使用。接口可以应用于任何UObject。此外,这意味着我们不再对对象和组件之间的“是一个”关系进行建模;相反,它只能表示“有一个”关系。

创建一个 UInterface

UInterfaces 是一对类,它们一起工作,使类能够在多个类层次结构中表现多态行为。本章向您展示了纯粹使用代码创建UInterface的基本步骤。

如何做...

  1. UInterfaces 不会出现在虚幻中的主类向导中,因此我们需要使用 Visual Studio 手动添加类。

  2. 解决方案资源管理器中右键单击文件夹,然后选择添加 | 新建项

  3. 选择一个.h文件开始,命名为MyInterface.h

  4. 确保将项目中项目的目录更改为 Intermediate 到 Source/ProjectName。

  5. 单击OK在项目文件夹中创建一个新的头文件。

  6. 重复步骤,以创建MyInterface.cpp作为您的实现文件。

  7. 将以下代码添加到头文件中:

#include "MyInterface.generated.h"
/**  */
UINTERFACE()
class UE4COOKBOOK_API UMyInterface: public UInterface
{
  GENERATED_BODY()
};

/**  */
class UE4COOKBOOK_API IMyInterface
{
  GENERATED_BODY()

  public:
  virtualFStringGetTestName();
};
  1. .cpp文件中使用以下代码实现类:
#include "UE4Cookbook.h"
#include "MyInterface.h"

FString IMyInterface::GetTestName()
{
  unimplemented();
  return FString();
}
  1. 编译您的项目以验证代码是否没有错误地编写。

它是如何工作的...

  1. UInterfaces 被实现为接口头文件中声明的一对类。

  2. 与往常一样,因为我们正在利用虚幻的反射系统,我们需要包含我们生成的头文件。有关更多信息,请参阅第五章中关于通过虚拟函数实现的事件处理,处理事件和委托。

  3. 与继承自UObject的类一样,它使用UCLASS,我们需要使用UINTERFACE宏来声明我们的新UInterface

  4. 该类被标记为UE4COOKBOOK_API,以帮助导出库符号。

  5. UObject部分的接口的基类是UInterface

  6. 就像UCLASS类型一样,我们需要在类的主体中放置一个宏,以便自动生成的代码被插入其中。

  7. 对于 UInterfaces,该宏是GENERATED_BODY()。该宏必须放在类主体的开头。

  8. 第二个类也被标记为UE4COOKBOOK_API,并且以特定的方式命名。

  9. 请注意,UInterface派生类和标准类具有相同的名称,但具有不同的前缀。UInterface派生类具有前缀U,标准类具有前缀I

  10. 这很重要,因为这是 Unreal Header Tool 期望类的命名方式,以使其生成的代码正常工作。

  11. 普通的本机接口类需要其自动生成的内容,我们使用GENERATED_BODY()宏包含它。

  12. 我们在IInterface内声明了类应该在内部实现的函数。

  13. 在实现文件中,我们实现了我们的UInterface的构造函数,因为它是由 Unreal Header Tool 声明的,并且需要一个实现。

  14. 我们还为我们的GetTestName()函数创建了一个默认实现。如果没有这个,编译的链接阶段将失败。这个默认实现使用unimplemented()宏,当代码行被执行时会发出调试断言。

另请参阅

  • 参考第五章中的使用委托绑定传递有效负载数据处理事件和委托;特别是第一个示例解释了我们在这里应用的一些原则

在对象上实现 UInterface

确保您已经按照前面的示例准备好要实现的UInterface

操作步骤...

  1. 使用 Unreal Wizard 创建一个名为SingleInterfaceActor的新的Actor类。

  2. IInterface—在本例中为IMyInterface—添加到我们新的Actor类的公共继承列表中:

class UE4COOKBOOK_API ASingleInterfaceActor : public AActor, public IMyInterface
  1. 为我们希望重写的IInterface函数在类中添加一个override声明:
FStringGetTestName() override;
  1. 通过添加以下代码在实现文件中实现重写的函数:
FStringASingleInterfaceActor::GetTestName()
{
  return IMyInterface::GetTestName();
}

工作原理...

  1. C++使用多重继承来实现接口,因此我们在这里利用了这种机制,声明了我们的SingleInterfaceActor类,其中添加了public IMyInterface

  2. 我们从IInterface而不是UInterface继承,以防止SingleInterfaceActor继承两个UObject的副本。

  3. 鉴于接口声明了一个virtual函数,如果我们希望自己实现它,我们需要使用 override 修饰符重新声明该函数。

  4. 在我们的实现文件中,我们实现了我们重写的virtual函数。

  5. 在我们的函数重写中,为了演示目的,我们调用函数的基本IInterface实现。或者,我们可以编写自己的实现,并完全避免调用基类的实现。

  6. 我们使用IInterface:: specifier而不是Super,因为Super指的是我们类的父类UClass,而 IInterfaces 不是 UClasses(因此没有U前缀)。

  7. 您可以根据需要在对象上实现第二个或多个 IInterfaces。

检查类是否实现了 UInterface

按照前两个示例,以便您有一个我们可以检查的UInterface,以及实现接口的类,可以对其进行测试。

操作步骤...

  1. 在您的游戏模式实现中,将以下代码添加到BeginPlay函数中:
FTransformSpawnLocation;
ASingleInterfaceActor* SpawnedActor = GetWorld()->SpawnActor<ASingleInterfaceActor> (ASingleInterfaceActor::StaticClass(), SpawnLocation);
if (SpawnedActor->GetClass()->ImplementsInterface(UMyInterface::StaticClass()))
{
  GEngine->AddOnScreenDebugMessage(-1, 1, FColor::Red, TEXT("Spawned actor implements interface!"));
}
  1. 鉴于我们引用了ASingleInterfaceActorIMyInterface,我们需要在我们的源文件中#include MyInterface.hSingleInterfaceActor.h

工作原理...

  1. BeginPlay中,我们创建一个空的FTransform函数,它的默认值是所有平移和旋转分量的0,因此我们不需要显式设置任何分量。

  2. 然后,我们使用UWorld中的SpawnActor函数,这样我们就可以创建我们的SingleActorInterface的实例,并将指针存储到临时变量中。

  3. 然后,我们使用GetClass()在我们的实例上获取一个引用到其关联的UClass。我们需要一个对UClass的引用,因为该对象是保存对象的所有反射数据的对象。

  4. 反射数据包括对象上所有UPROPERTY的名称和类型,对象的继承层次结构,以及它实现的所有接口的列表。

  5. 因此,我们可以在UClass上调用ImplementsInterface(),如果对象实现了所讨论的UInterface,它将返回true

  6. 如果对象实现了接口,因此从ImplementsInterface返回true,我们就会在屏幕上打印一条消息。

另请参阅

  • 第五章, 处理事件和委托,有许多与生成 actor 相关的配方

在本机代码中实现 UInterface 的转换

作为开发人员,UInterfaces 为您提供的一个优势是,使用Cast< >来处理转换,可以将实现共同接口的异构对象集合视为相同对象的集合。

注意

请注意,如果您的类通过 Blueprint 实现接口,则此方法将无效。

准备工作

您应该为此配方准备一个UInterface和一个实现接口的Actor

使用 Unreal 中的向导创建一个新的游戏模式,或者可选地,重用以前配方中的项目和GameMode

操作步骤...

  1. 打开游戏模式的声明,并向其中添加一个新的UPROPERTY()宏
UPROPERTY()
TArray<IMyInterface*>MyInterfaceInstances;
  1. 在头文件的包含部分添加#include "MyInterface.h"

  2. 在游戏模式的BeginPlay实现中添加以下内容:

for (TActorIterator<AActor> It(GetWorld(), AActor::StaticClass()); It; ++It)
{
  AActor* Actor = *It;
  IMyInterface* MyInterfaceInstance = Cast<IMyInterface>(Actor);
  if (MyInterfaceInstance)
  {
    MyInterfaceInstances.Add(MyInterfaceInstance);
  }
}
GEngine->AddOnScreenDebugMessage(-1, 1, FColor::Red, FString::Printf(TEXT("%d actors implement the interface"), MyInterfaceInstances.Num()));
  1. 将级别的游戏模式覆盖设置为您的游戏模式,然后将几个实现自定义接口的 actor 实例拖放到级别中。

  2. 当您播放级别时,屏幕上应该打印一条消息,指示在级别中实现了接口的实例的数量:操作步骤...

它是如何工作的...

  1. 我们创建了一个指向MyInterface实现的指针数组。

  2. BeginPlay中,我们使用TActorIterator<AActor>来获取我们级别中的所有Actor实例。

  3. TActorIterator有以下构造函数:

explicitTActorIterator( UWorld* InWorld, TSubclassOf<ActorType>InClass = ActorType::StaticClass() )
: Super(InWorld, InClass )
  1. TActorIterator期望一个要操作的世界,以及一个UClass实例来指定我们感兴趣的 Actor 类型。

  2. ActorIterator是类似 STL 迭代器类型的迭代器。这意味着我们可以编写以下形式的for循环:

for (iterator-constructor;iterator;++iterator)
  1. 在循环内,我们取消引用迭代器以获取Actor指针。

  2. 然后,我们尝试将其转换为我们的接口;如果它实现了它,这将返回一个指向接口的指针,否则将返回nullptr

  3. 因此,我们可以检查接口指针是否为null,如果不是,我们可以将接口指针引用添加到我们的数组中。

  4. 最后,一旦我们遍历了TActorIterator中的所有 actor,我们就可以在屏幕上显示一条消息,显示实现了接口的项目的计数。

从 C++调用本机 UInterface 函数

按照前一个配方来理解将Actor指针转换为接口指针。

注意

请注意,由于此配方依赖于前一个配方中使用的转换技术,因此它只能与使用 C++实现接口的对象一起使用,而不能与 Blueprint 一起使用。这是因为 Blueprint 类在编译时不可用,因此在技术上不继承该接口。

操作步骤...

  1. 使用编辑向导创建一个新的Actor类。将其命名为AntiGravityVolume

  2. BoxComponent添加到新的Actor中。

UPROPERTY()
UBoxComponent* CollisionComponent;
  1. 在头文件中重写以下Actor virtual函数:
virtual void NotifyActorBeginOverlap(AActor* OtherActor) override;
virtual void NotifyActorEndOverlap(AActor* OtherActor) override;
  1. 在源文件中创建一个实现,如下所示:
voidAAntiGravityVolume::NotifyActorBeginOverlap(AActor* OtherActor)
{
  IGravityObject* GravityObject = Cast<IGravityObject>(OtherActor);
  if (GravityObject != nullptr)
  {
    GravityObject->DisableGravity();
  }
}

voidAAntiGravityVolume::NotifyActorEndOverlap(AActor* OtherActor)
{
  IGravityObject* GravityObject = Cast<IGravityObject>(OtherActor);
  if (GravityObject != nullptr)
  {
    GravityObject->EnableGravity();
  }
}
  1. 在构造函数中初始化BoxComponent
AAntiGravityVolume::AAntiGravityVolume()
{
  PrimaryActorTick.bCanEverTick = true;
  CollisionComponent = CreateDefaultSubobject<UBoxComponent>("CollisionComponent");
  CollisionComponent->SetBoxExtent(FVector(200, 200, 400));
  RootComponent = CollisionComponent;

}
  1. 创建一个名为GravityObject的接口。

  2. IGravityObject中添加以下virtual函数:

virtual void EnableGravity();
virtual void DisableGravity();
  1. IGravityObject实现文件中创建virtual函数的默认实现:
voidIGravityObject::EnableGravity()
{
  AActor* ThisAsActor = Cast<AActor>(this);
  if (ThisAsActor != nullptr)
  {
    TArray<UPrimitiveComponent*>PrimitiveComponents;
    ThisAsActor->GetComponents(PrimitiveComponents);
    for (UPrimitiveComponent* Component : PrimitiveComponents)
    {
      Component->SetEnableGravity(true);
    }
  }
}

voidIGravityObject::DisableGravity()
{
  AActor* ThisAsActor = Cast<AActor>(this);
  if (ThisAsActor != nullptr)
  {
    TArray<UPrimitiveComponent*>PrimitiveComponents;
    ThisAsActor->GetComponents(PrimitiveComponents);
    for (UPrimitiveComponent* Component : PrimitiveComponents)
    {
      Component->SetEnableGravity(false);
    }
  }
}
  1. 创建一个名为PhysicsCubeActor子类。

  2. 添加一个静态网格:

UPROPERTY()
UStaticMeshComponent* MyMesh;
  1. 在构造函数中初始化组件:
MyMesh = CreateDefaultSubobject<UStaticMeshComponent>("MyMesh");
autoMeshAsset = ConstructorHelpers::FObjectFinder<UStaticMesh>(TEXT("StaticMesh'/Engine/BasicShapes/Cube.Cube'"));
if (MeshAsset.Object != nullptr)
{
  MyMesh->SetStaticMesh(MeshAsset.Object);
}
MyMesh->SetMobility(EComponentMobility::Movable);
MyMesh->SetSimulatePhysics(true);
SetActorEnableCollision(true);
  1. 要使PhysicsCube实现GravityObject,首先在头文件中#include "GravityObject.h",然后修改类声明:
class UE4COOKBOOK_API APhysicsCube : public AActor, public IGravityObject
  1. 编译您的项目。

  2. 创建一个新的关卡,并在场景中放置一个重力体积的实例。

  3. 在重力体积上放置一个PhysicsCube的实例,然后稍微旋转它,使其有一个角落比其他角落低,如下图所示:操作步骤...

  4. 验证当对象进入体积时重力被关闭,然后再次打开。

注意

请注意,重力体积不需要知道任何关于您的PhysicsCube actor 的信息,只需要知道重力对象接口。

工作原理...

  1. 我们创建一个新的Actor类,并添加一个箱子组件,以便给角色添加一个会与角色发生碰撞的物体。或者,如果您想要使用 BSP 功能来定义体积的形状,您也可以对AVolume进行子类化。

  2. 重写NotifyActorBeginOverlapNotifyActorEndOverlap,以便在对象进入或离开AntiGravityVolume区域时执行某些操作。

  3. NotifyActorBeginOverlap实现中,我们尝试将与我们发生重叠的对象转换为IGravityObject指针。

  4. 这个测试是为了检查所讨论的对象是否实现了该接口。

  5. 如果指针有效,则对象确实实现了接口,因此可以安全地使用接口指针调用对象上的接口方法。

  6. 鉴于我们在NotifyActorBeginOverlap内部,我们希望禁用对象上的重力,因此我们调用DisableGravity()

  7. NotifyActorEndOverlap内部,我们执行相同的检查,但是我们重新启用了对象的重力。

  8. DisableGravity的默认实现中,我们将我们自己的指针(this指针)转换为AActor

  9. 这使我们能够确认接口仅在Actor子类上实现,并调用在AActor中定义的方法。

  10. 如果指针有效,我们知道我们是一个Actor,所以我们可以使用GetComponents<class ComponentType>()来从自身获取特定类型的所有组件的TArray

  11. GetComponents是一个template函数。它需要一些模板参数:

template<class T, class AllocatorType>
voidGetComponents(TArray<T*, AllocatorType>&OutComponents) const
  1. 自 2014 年标准以来,C++支持模板参数的编译时推断。这意味着如果编译器可以从我们提供的普通函数参数中推断出模板参数,那么在调用函数时我们不需要实际指定模板参数。

  2. TArray的默认实现是template<typename T, typename Allocator = FDefaultAllocator>TArray;

  3. 这意味着我们不需要默认情况下指定分配器,因此当我们声明数组时,我们只使用TArray<UPrimitiveComponent*>

  4. TArray传递到GetComponents函数中时,编译器知道它实际上是TArray<UPrimitiveComponent*, FDefaultAllocator>,并且能够填充模板参数TAllocatorType,所以在函数调用时不需要这两个作为模板参数。

  5. GetComponents遍历Actor拥有的组件,并且从typename T继承的任何组件都有指针存储在PrimitiveComponents数组中。

  6. 使用基于范围的for循环,这是 C++的另一个新特性,我们可以在不需要使用传统的for循环结构的情况下迭代函数放入我们的TArray中的组件。

  7. 对每个组件调用SetEnableGravity(false),这将禁用重力。

  8. 同样,EnableGravity函数遍历了 actor 中包含的所有 primitive 组件,并使用SetEnableGravity(true)启用了重力。

另请参阅

  • 查看第四章, Actors and Components, 详细讨论了演员和组件。第五章, 处理事件和委托, 讨论了诸如NotifyActorOverlap之类的事件。

相互继承 UInterface

有时,您可能需要创建一个更通用的UInterface专门用于UInterface

这个配方向您展示了如何使用 UInterfaces 继承来专门化一个Killable接口,使其具有无法通过正常手段杀死的Undead接口。

操作步骤...

  1. 创建一个名为UKillableUINTERFACE/IInterface

  2. UInterface声明中添加UINTERFACE(meta=(CannotImplementInterfaceInBlueprint))

  3. 在头文件中添加以下函数:

UFUNCTION(BlueprintCallable, Category=Killable)
virtual bool IsDead();
UFUNCTION(BlueprintCallable, Category = Killable)
virtual void Die();
  1. 在实现文件中为接口提供默认实现:
boolIKillable::IsDead()
{
  return false;
}

voidIKillable::Die()
{
  GEngine->AddOnScreenDebugMessage(-1,1, FColor::Red,"Arrrgh");
  AActor* Me = Cast<AActor>(this);
  if (Me)
  {
    Me->Destroy();
  }

}
  1. 创建一个新的UINTERFACE/IInterface称为Undead。修改它们继承自UKillable/IKillable
UINTERFACE()
class UE4COOKBOOK_API UUndead: public UKillable
{
  GENERATED_BODY()
};

/**  */
class UE4COOKBOOK_API IUndead: public IKillable
{
  GENERATED_BODY()

};
  1. 确保您包含了定义Killable接口的头文件。

  2. 在新接口中添加一些重写和新的方法声明:

virtual bool IsDead() override;
virtual void Die() override;
virtual void Turn();
virtual void Banish();
  1. 为函数创建实现:
boolIUndead::IsDead()
{
  return true;
}

voidIUndead::Die()
{
  GEngine->AddOnScreenDebugMessage(-1,1, FColor::Red,"You can't kill what is already dead. Mwahaha");
}

voidIUndead::Turn()
{
  GEngine->AddOnScreenDebugMessage(-1,1, FColor::Red, "I'm fleeing!");

}

voidIUndead::Banish()
{
  AActor* Me = Cast<AActor>(this);
  if (Me)
  {
    Me->Destroy();
  }
}
  1. 在 C++中创建两个新的Actor类:一个名为Snail,另一个名为Zombie

  2. Snail类设置为实现IKillable接口,并添加适当的头文件#include

  3. 同样,将Zombie类设置为实现IUndead,并#include "Undead.h"

  4. 编译您的项目。

  5. 启动编辑器,将ZombieSnail的实例拖入你的关卡中。

  6. 关卡蓝图中为它们添加引用。

  7. 在每个引用上调用Die(消息)。操作步骤...

  8. 连接两个消息调用的执行引脚,然后将其连接到Event BeginPlay

运行游戏,然后验证Zombie对您的杀死尝试不屑一顾,但Snail呻吟着然后死去(从世界大纲中移除)。

操作步骤...

工作原理...

  1. 为了能够在关卡蓝图中测试这个配方,我们需要使接口函数可以通过蓝图调用,所以我们需要在我们的UFUNCTION上加上BlueprintCallable修饰符。

  2. 然而,在UInterface中,编译器默认期望接口可以通过 C++和蓝图实现。这与BlueprintCallable冲突,后者仅表示该函数可以从蓝图中调用,而不是可以在其中被重写。

  3. 我们可以通过将接口标记为CannotImplementInterfaceInBlueprint来解决冲突。

  4. 这使得我们可以使用BlueprintCallable作为我们的UFUNCTION修饰符,而不是BlueprintImplementableEvent(由于额外的代码允许通过蓝图重写函数而产生额外的开销)。

  5. 我们将IsDeadDie定义为virtual,以使它们可以在另一个继承此类的 C++类中被重写。

  6. 在我们的默认接口实现中,IsDead总是返回false

Die的默认实现在屏幕上打印死亡消息,然后销毁实现此接口的对象(如果它是一个Actor)。

  1. 现在我们可以创建一个名为Undead的第二个接口,它继承自Killable

  2. 我们在类声明中使用public UKillable/public IKillable来表示这一点。

  3. 当然,结果是我们需要包含定义Killable接口的头文件。

  4. 我们的新接口重写了Killable定义的两个函数,以提供更合适的UndeadIsDead/Die定义。

  5. 我们的重写定义已经通过从IsDead返回true来使Undead已经死亡。

  6. DieUndead上调用时,我们只是打印一条消息,Undead嘲笑我们试图再次杀死它的微弱尝试,并且什么也不做。

  7. 我们还可以为我们的Undead特定函数指定默认实现,即Turn()Banish()

  8. Undead被转化时,它们会逃跑,为了演示目的,我们在屏幕上打印一条消息。

  9. 然而,如果Undead被放逐,它们将被消灭并毁灭得无影无踪。

  10. 为了测试我们的实现,我们创建了两个Actors,每个都继承自两个接口中的一个。

  11. 在我们的级别中添加每个角色的一个实例后,我们使用级别蓝图来访问级别的BeginPlay事件。

  12. 当关卡开始播放时,我们使用消息调用来尝试在我们的实例上调用Die函数。

  13. 打印出来的消息是不同的,并且对应于两个函数实现,显示了 Zombie 对Die的实现是不同的,并且已经覆盖了 Snail 的实现。

在 C++中重写 UInterface 函数

UInterfaces 允许 C++中的继承的一个副作用是,我们可以在子类以及蓝图中覆盖默认实现。这个操作步骤向你展示了如何做到这一点。

准备工作

按照从 C++调用本机 UInterface 函数的步骤创建一个 Physics Cube,以便你已经准备好这个类。

操作步骤...

  1. 创建一个名为Selectable的新接口。

  2. ISelectable中定义以下函数:

virtual bool IsSelectable();

virtual bool TrySelect();

virtual void Deselect();
  1. 为这样的函数提供默认实现:
boolISelectable::IsSelectable()
{
  GEngine->AddOnScreenDebugMessage(-1, 1, FColor::Red, "Selectable");
  return true;
}

boolISelectable::TrySelect()
{
  GEngine->AddOnScreenDebugMessage(-1, 1, FColor::Red, "Accepting Selection");
  return true;
}

voidISelectable::Deselect()
{
  unimplemented();
}
  1. 创建一个基于APhysicsCube的类,名为SelectableCube

  2. SelectableCube类的头文件中包含#include "Selectable.h"

  3. 修改ASelectableCube的声明如下:

class UE4COOKBOOK_API ASelectableCube : public APhysicsCube, public ISelectable
  1. 将以下函数添加到头文件中:
ASelectableCube();
virtual void NotifyHit(class UPrimitiveComponent* MyComp, AActor* Other, class UPrimitiveComponent* OtherComp, bool bSelfMoved, FVectorHitLocation, FVectorHitNormal, FVectorNormalImpulse, constFHitResult& Hit) override;
  1. 实现以下函数:
ASelectableCube::ASelectableCube()
: Super()
{
  MyMesh->SetNotifyRigidBodyCollision(true);
}

voidASelectableCube::NotifyHit(class UPrimitiveComponent* MyComp, AActor* Other, class UPrimitiveComponent* OtherComp, bool bSelfMoved, FVectorHitLocation, FVectorHitNormal, FVectorNormalImpulse, constFHitResult& Hit)
{
  if (IsSelectable())
  {
    TrySelect();
  }
}
  1. 创建一个名为NonSelectableCube的新类,它继承自SelectableCube

  2. NonSelectableCube应该覆盖SelectableInterface中的函数:

virtual bool IsSelectable() override;

virtual bool TrySelect() override;

virtual void Deselect() override;
  1. 实现文件应该被修改以包括以下内容:
boolANonSelectableCube::IsSelectable()
{
  GEngine->AddOnScreenDebugMessage(-1, 1, FColor::Red, "Not Selectable");
  return false;
}

boolANonSelectableCube::TrySelect()
{
  GEngine->AddOnScreenDebugMessage(-1, 1, FColor::Red, "Refusing Selection");
  return false;
}

voidANonSelectableCube::Deselect()
{
  unimplemented();
}
  1. SelectableCube的实例放置在离地面一定范围的级别中,并播放游戏。当方块触地时,您应该收到验证该角色可选择并已接受选择的消息。操作步骤...

  2. 删除SelectableCube并用NonSelectableCube的实例替换,以查看替代消息,指示该角色不可选择,并拒绝选择。

它是如何工作的...

  1. 我们在Selectable接口中创建了三个函数。

  2. IsSelectable返回一个布尔值,表示对象是否可选择。你可以避免这样做,只需使用TrySelect,因为它返回一个布尔值来表示成功,但是,例如,你可能想知道你的 UI 内的对象是否是有效的选择,而不必实际尝试。

  3. TrySelect实际上尝试选择对象。没有明确的合同强制用户在尝试选择对象时尊重IsSelectable,因此TrySelect的命名是为了传达选择可能并不总是成功。

  4. 最后,Deselect是一个添加的函数,允许对象处理失去玩家选择。这可能涉及更改 UI 元素,停止声音或其他视觉效果,或者只是从单位周围移除选择轮廓。

  5. 函数的默认实现返回true表示IsSelectable(默认情况下,任何对象都是可选择的),返回true表示TrySelect(选择尝试总是成功),如果在没有被类实现的情况下调用Deselect,则会发出调试断言。

  6. 如果愿意,也可以将Deselect实现为纯虚函数。

  7. SelectableCube是一个新的类,继承自PhysicsCube,同时实现了ISelectable接口。

  8. 它还覆盖了NotifyHit,这是在AActor中定义的一个virtual函数,当演员经历RigidBody碰撞时触发。

  9. 我们在SelectableCube的实现中使用Super()构造函数调用来调用PhysicsCube的构造函数。然后,我们添加我们自己的实现,它在我们的静态网格实例上调用SetNotifyRigidBodyCollision(true)。这是必要的,因为默认情况下,刚体(例如具有碰撞的PrimitiveComponents)不会触发Hit事件,以进行性能优化。因此,我们重写的NotifyHit函数将永远不会被调用。

  10. NotifyHit的实现中,我们在自身上调用了一些ISelectable接口函数。鉴于我们知道我们是从ISelectable继承的对象,我们无需转换为ISelectable*即可调用它们。

  11. 我们使用IsSelectable来检查对象是否可选择,如果是,则尝试使用TrySelect来实际执行选择。

  12. NonSelectableCube继承自SelectableCube,因此我们可以强制该对象永远不可选择。

  13. 我们通过再次重写ISelectable接口函数来实现这一点。

  14. ANonSelectableCube::IsSelectable()中,我们在屏幕上打印一条消息,以便我们可以验证该函数是否被调用,然后返回false以指示该对象根本不可选择。

  15. 如果用户不尊重IsSelectable()ANonSelectableCube::TrySelect()始终返回false,以指示选择不成功。

  16. 鉴于不可能选择NonSelectableCubeDeselect()调用unimplemented(),这会引发一个断言警告,指出该函数未被实现。

  17. 现在,在播放场景时,每当SelectableCube/NonSelectableCube撞击另一个物体,导致刚体碰撞时,相关的角色将尝试选择自己,并在屏幕上打印消息。

另请参阅

  • 参见第六章,输入和碰撞,其中向您展示了如何从鼠标光标向游戏世界进行射线投射,并且可以用于扩展此示例以允许玩家点击物品进行选择

从本地基类向蓝图公开 UInterface 方法

能够在 C++中定义UInterface方法非常好,但它们也应该从蓝图中可访问。否则,使用蓝图的设计师或其他人将无法与您的UInterface进行交互。本示例向您展示了如何使接口中的函数在蓝图系统中可调用。

如何做...

  1. 创建一个名为UPostBeginPlay/IPostBeginPlayUInterface

  2. IPostBeginPlay添加以下virtual方法:

UFUNCTION(BlueprintCallable, Category=Test)
virtual void OnPostBeginPlay();
  1. 提供函数的实现:
voidIPostBeginPlay::OnPostBeginPlay()
{
  GEngine->AddOnScreenDebugMessage(-1, 1, FColor::Red, "PostBeginPlay called");
}
  1. 创建一个名为APostBeginPlayTest的新的Actor类。

  2. 修改类声明,使其还继承IPostBeginPlay

UCLASS()
class UE4COOKBOOK_API APostBeginPlayTest : public AActor, public IPostBeginPlay
  1. 编译您的项目。在编辑器内,将APostBeginPlayTest的实例拖入您的级别中。选择该实例,单击打开级别蓝图如何做...

  2. 在级别蓝图内,右键单击并创建对 PostBeginPlayTest1 的引用如何做...

  3. 从 actor 引用的右侧蓝色引脚拖动,然后在上下文菜单中搜索onpost,以查看您的新接口函数是否可用。单击它以在蓝图中插入对本机UInterface实现的调用。如何做...

  4. 最后,将BeginPlay节点的执行引脚(白色箭头)连接到OnPostBeginPlay的执行引脚。如何做...

  5. 当您播放级别时,您应该看到屏幕上出现PostBeginPlay called的消息,验证蓝图已成功访问并调用了您的UInterface的本地代码实现。

它是如何工作的...

  1. UINTERFACE/IInterface对在其他示例中的功能一样,UInterface包含反射信息和其他数据,而IInterface作为实际的接口类,可以被继承。

  2. 允许IInterface内部函数暴露给蓝图的最重要的元素是UFUNCTION修饰符。

  3. BlueprintCallable标记此函数可以从蓝图系统中调用。

  4. 以任何方式暴露给蓝图的函数也需要一个Category值。这个Category值指定了函数在上下文菜单中将被列在哪个标题下。

  5. 该函数还必须标记为virtual,这样通过本地代码实现接口的类可以重写其中的函数实现。如果没有virtual修饰符,虚幻头部工具将给出一个错误,指示您必须添加virtualBlueprintImplementableEvent作为UFUNCTION修饰符。

  6. 这样做的原因是,如果没有这两者中的任何一个,接口函数将无法在 C++中被重写(由于缺少virtual),或者在蓝图中(因为缺少BlueprintImplementableEvent)。一个不能被重写,只能被继承的接口具有有限的实用性,因此 Epic 选择不在 UInterfaces 中支持它。

  7. 然后,我们提供了OnPostBeginPlay函数的默认实现,它使用GEngine指针来显示一个调试消息,确认函数被调用。

另请参阅

  • 有关如何将 C++类与蓝图集成的多个示例,请参阅第八章集成 C++和虚幻编辑器

在蓝图中实现 UInterface 函数

虚幻中 UInterface 的一个关键优势是用户能够在编辑器中实现UInterface函数。这意味着接口可以严格在蓝图中实现,而不需要任何 C++代码,这对设计师来说是有帮助的。

如何操作...

  1. 创建一个名为AttackAvoider的新UInterface

  2. 将以下函数声明添加到头文件:

UFUNCTION(BlueprintImplementableEvent, BlueprintCallable, Category = AttackAvoider)
voidAttackIncoming(AActor* AttackActor);
  1. 在编辑器中创建一个新的蓝图类如何操作...

  2. 将类基于Actor如何操作...

  3. 打开类设置如何操作...

  4. 单击实现接口的下拉菜单,并选择AttackAvoider如何操作...

  5. 编译您的蓝图:如何操作...

  6. 在事件图中右键单击,输入event attack。在上下文敏感菜单中,您应该看到Event Attack Incoming。选择它以在图表中放置一个事件节点:如何操作...

  7. 从新节点的执行引脚中拖出,并释放。在上下文敏感菜单中输入print string以添加一个Print String节点。如何操作...

  8. 您现在已经在蓝图中实现了一个UInterface函数。

工作原理...

  1. UINTERFACE/IInterface的创建方式与本章其他示例中看到的完全相同。

  2. 然而,当我们向接口添加一个函数时,我们使用一个新的UFUNCTION修饰符:BlueprintImplementableEvent

  3. BlueprintImplementableEvent 告诉虚幻头部工具生成代码,创建一个空的存根函数,可以由蓝图实现。我们不需要为函数提供默认的 C++实现。

  4. 我们在蓝图中实现接口,这样就可以以一种允许我们在蓝图中定义其实现的方式暴露函数。

  5. 头部工具生成的自动生成代码将UInterface函数的调用转发到我们的蓝图实现。

另请参阅

  • 以下示例向您展示了如何在 C++中为您的UInterface函数定义默认实现,然后在必要时在蓝图中进行覆盖

创建 C++ UInterface 函数实现,可以在蓝图中被覆盖

与以前的示例一样,UInterfaces 很有用,但如果设计者无法使用其功能,那么其效用将受到严重限制。

上一个示例向您展示了如何从蓝图中调用 C++ UInterface函数;这个示例将向您展示如何用自己的自定义蓝图函数替换UInterface函数的实现。

操作步骤...

  1. 创建一个名为WearableIWearableUWearable)的新接口。

  2. 在头文件中添加以下函数:

UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = Wearable)
int32GetStrengthRequirement();
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = Wearable)
boolCanEquip(APawn* Wearer);
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = Wearable)
voidOnEquip(APawn* Wearer);
  1. 在实现文件中添加以下函数实现:
int32 IWearable::GetStrengthRequirement_Implementation()
{
  return 0;
}

Bool IWearable::CanEquip_Implementation(APawn* Wearer)
{
  return true;
}

Void IWearable::OnEquip_Implementation(APawn* Wearer)
{

}
  1. 在编辑器中创建一个名为Boots的新Actor类。

  2. Boots的头文件中添加#include "Wearable.h"

  3. 修改类声明如下:

UCLASS()
class UE4COOKBOOK_API ABoots : public AActor, public IWearable
  1. 添加我们接口创建的纯virtual函数的以下实现:
virtual void OnEquip_Implementation(APawn* Wearer) override
{
  IWearable::OnEquip_Implementation(Wearer);
}
virtual bool CanEquip_Implementation(APawn* Wearer) override
{
  return IWearable::CanEquip_Implementation(Wearer);
}
virtual int32 GetStrengthRequirement_Implementation() override
{
  return IWearable::GetStrengthRequirement_Implementation();
}
  1. 创建一个基于Actor的名为Gloves的新蓝图类。

  2. 在类设置中,选择Wearable作为Gloves角色将实现的接口。

  3. Gloves中,像这样重写OnEquip函数:操作步骤...

  4. GlovesBoots的副本拖到您的级别中进行测试。

  5. 在您的级别中添加以下蓝图代码:操作步骤...

  6. 验证Boots执行默认行为,但Gloves执行蓝图定义的行为。操作步骤...

工作原理...

  1. 这个示例同时使用了两个UFUNCTION修饰符:BlueprintNativeEventBlueprintCallable

  2. BlueprintCallable在以前的示例中已经展示过,它是一种将UFUNCTION标记为在蓝图编辑器中可见和可调用的方法。

  3. BlueprintNativeEvent表示一个具有默认 C++(本机代码)实现的UFUNCTION,但也可以在蓝图中被覆盖。它是虚函数和BlueprintImplementableEvent的组合。

  4. 为了使这种机制工作,虚幻头部工具生成函数的主体,以便如果存在函数的蓝图版本,则调用该函数的蓝图版本;否则,将方法调用分派到本机实现。

  5. 为了将默认实现与分发功能分开,UHT 定义了一个新函数,该函数以您声明的函数命名,但在末尾添加了_Implementation

  6. 这就是为什么头文件声明了GetStrengthRequirement,但没有实现,因为那是自动生成的。

  7. 这也是为什么您的实现文件定义了GetStrengthRequirement_Implementation,但没有声明它,因为它也是自动生成的。

  8. Boots类实现了IWearable,但没有覆盖默认功能。但是,因为_Implementation函数被定义为virtual,我们仍然需要显式实现接口函数,然后直接调用默认实现。

  9. 相比之下,Gloves也实现了IWearable,但在蓝图中为OnEquip定义了一个重写的实现。

  10. 当我们使用级别蓝图调用这两个角色的OnEquip时,可以验证这一点。

从 C++调用蓝图定义的接口函数

虽然以前的示例侧重于 C++在蓝图中的可用性,比如能够从蓝图中调用 C++函数,并用蓝图覆盖 C++函数,但这个示例展示了相反的情况:从 C++调用蓝图定义的接口函数。

操作步骤...

  1. 创建一个名为UTalker/ITalker的新UInterface

  2. 添加以下UFUNCTION实现:

UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = Talk)
void StartTalking();
  1. .cpp文件中提供一个默认的空实现:
void ITalker::StartTalking_Implementation()
{

}
  1. 创建一个基于StaticMeshActor的新类。

  2. 添加#include并修改类声明以包括 talker 接口:

#include "Talker.h"
class UE4COOKBOOK_API ATalkingMesh : public AStaticMeshActor, public ITalker
  1. 还要在类声明中添加以下函数:
void StartTalking_Implementation();
  1. 在实现中,将以下内容添加到构造函数中:
ATalkingMesh::ATalkingMesh()
:Super()
{
  autoMeshAsset = ConstructorHelpers::FObjectFinder<UStaticMesh>(TEXT("StaticMesh'/Engine/BasicShapes/Cube.Cube'"));
  if (MeshAsset.Object != nullptr)
  {
    GetStaticMeshComponent()->SetStaticMesh(MeshAsset.Object);
    //GetStaticMeshComponent()->SetCollisionProfileName(UCollisionProfile::Pawn_ProfileName);
    GetStaticMeshComponent()->bGenerateOverlapEvents = true;
  }
  GetStaticMeshComponent()->SetMobility(EComponentMobility::Movable);
  SetActorEnableCollision(true);
}
Implmement the default implementation of our StartTalking function:
voidATalkingMesh::StartTalking_Implementation()
{
  GEngine->AddOnScreenDebugMessage(-1, 1, FColor::Red, TEXT("Hello there. What is your name?"));
}
  1. 创建一个基于DefaultPawn的新类,作为我们的玩家角色的功能。

  2. 在我们的类头文件中添加一些UPROPERTY/UFUNCTION

UPROPERTY()
UBoxComponent* TalkCollider;
UFUNCTION()
voidOnTalkOverlap(AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, constFHitResult&SweepResult);
  1. 修改构造函数:
ATalkingPawn::ATalkingPawn()
:Super()
{
  // Set this character to call Tick() every frame. You can turn this off to improve performance if you don't need it.
  PrimaryActorTick.bCanEverTick = true;
  TalkCollider = CreateDefaultSubobject<UBoxComponent>("TalkCollider"); 
  TalkCollider->SetBoxExtent(FVector(200, 200, 100));
  TalkCollider->OnComponentBeginOverlap.AddDynamic(this, &ATalkingPawn::OnTalkOverlap);
  TalkCollider->AttachTo(RootComponent);
}
  1. 实现OnTalkOverlap
voidATalkingPawn::OnTalkOverlap(AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, constFHitResult&SweepResult)
{
  if (OtherActor->GetClass()->ImplementsInterface(UTalker::StaticClass()))
  {
    ITalker::Execute_StartTalking(OtherActor);
  }
}
  1. 创建一个新的GameMode,并将TalkingPawn设置为玩家的默认 pawn 类。

  2. 将您的ATalkingMesh类的一个实例拖入级别中。

  3. 通过右键单击它并从上下文菜单中选择适当的选项,基于ATalkingMesh创建一个新的蓝图类:如何做...

  4. 将其命名为MyTalkingMesh

  5. 在蓝图编辑器中,创建一个像这样的StartTalking实现:如何做...

  6. 将您的新蓝图的副本拖入级别中,放在您的ATalkingMesh实例旁边。

  7. 走近这两个演员,并验证您的自定义 Pawn 是否正确调用了默认的 C++实现或蓝图实现。如何做...

它是如何工作的...

  1. 一如既往,我们创建一个新的接口,然后在IInterface类中添加一些函数定义。

  2. 我们使用BlueprintNativeEvent说明符来指示我们希望在 C++中声明一个默认实现,然后可以在蓝图中进行重写。

  3. 我们创建了一个新的类(从StaticMeshActor继承以方便起见),并在其上实现了接口。

  4. 在新类构造函数的实现中,我们加载了一个静态网格,并像往常一样设置了我们的碰撞。

  5. 然后我们为我们的接口函数添加了一个实现,它只是在屏幕上打印一条消息。

  6. 如果您在一个完整的项目中使用这个,您可以播放动画,播放音频,修改用户界面,以及其他必要的操作来开始与您的Talker对话。

  7. 然而,此时,我们实际上没有任何东西来调用我们的Talker上的StartTalking

  8. 实现这一点的最简单方法是创建一个新的Pawn子类(再次从DefaultPawn继承以方便起见),它可以开始与任何与之发生碰撞的Talker演员交谈。

  9. 为了使其工作,我们创建了一个新的BoxComponent来建立我们将触发对话的半径。

  10. 一如既往,这是一个UPROPERTY,因此它不会被垃圾回收。

  11. 我们还为一个函数创建了定义,当新的BoxComponent与场景中的另一个Actor重叠时将被触发。

  12. 我们的TalkingPawn的构造函数初始化了新的BoxComponent,并适当设置了其范围。

  13. 构造函数还将OnTalkOverlap函数绑定为事件处理程序,以处理与我们的BoxComponent发生碰撞。

  14. 它还将盒组件附加到我们的RootComponent,以便随着玩家在级别中移动而移动。

  15. OnTalkOverlap内部,我们需要检查另一个演员是否实现了与我们的盒子重叠的Talker接口。

  16. 最可靠的方法是使用UClass中的ImplementsInterface函数。这个函数使用 Unreal Header Tool 在编译期间生成的类信息,并正确处理 C++和蓝图实现的接口。

  17. 如果函数返回true,我们可以使用我们的IInterface中包含的特殊自动生成的函数来调用我们实例上所选择的接口方法。

  18. 这是一个形式为<IInterface>::Execute_<FunctionName>的静态方法。在我们的实例中,我们的IInterfaceITalker,函数是StartTalking,所以我们要调用的函数是ITalker::Execute_StartTalking()

  19. 我们需要这个函数的原因是,当一个接口在蓝图中实现时,关系实际上并没有在编译时建立。因此,C++并不知道接口已经实现,因此我们无法将蓝图类转换为IInterface以直接调用函数。

  20. Execute_函数接受实现接口的对象的指针,并调用一些内部方法来调用所需函数的蓝图实现。

  21. 当您播放级别并四处走动时,自定义的Pawn会不断接收到当其BoxComponent与其他对象重叠时的通知。

  22. 如果它们实现了UTalker/ITalker接口,Pawn 然后尝试在相关的Actor实例上调用StartTalking,然后在屏幕上打印适当的消息。

使用 UInterfaces 实现一个简单的交互系统

本教程将向您展示如何将本章中的一些其他教程组合起来,以演示一个简单的交互系统和一个带有可交互门铃的门,以打开门。

如何操作...

  1. 创建一个新的接口Interactable

  2. 将以下函数添加到IInteractable类声明中:

UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category=Interactable)
boolCanInteract();
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = Interactable)
voidPerformInteract();
  1. 在实现文件中为两个函数创建默认实现:
boolIInteractable::CanInteract_Implementation()
{
  return true;
}

voidIInteractable::PerformInteract_Implementation()
{

}
  1. 创建第二个接口Openable

  2. 将此函数添加到其声明中:

UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category=Openable)
void Open();
  1. Interactable一样,为Open函数创建一个默认实现:
voidIOpenable::Open_Implementation()
{
}
  1. 创建一个名为DoorBell的新类,基于StaticMeshActor

  2. DoorBell.h#include "Interactable.h",并在类声明中添加以下函数:

virtual bool CanInteract_Implementation() override;
virtual void PerformInteract_Implementation() override;
UPROPERTY(BlueprintReadWrite, EditAnywhere)
AActor* DoorToOpen;
private:
boolHasBeenPushed;
  1. DoorBell.cpp文件中,#include "Openable.h"

  2. 在构造函数中为我们的DoorBell加载一个静态网格:

HasBeenPushed = false;
autoMeshAsset = ConstructorHelpers::FObjectFinder<UStaticMesh>(TEXT("StaticMesh'/Engine/BasicShapes/Cube.Cube'"));
if (MeshAsset.Object != nullptr)
{
  GetStaticMeshComponent()->SetStaticMesh(MeshAsset.Object);
  //GetStaticMeshComponent()->SetCollisionProfileName(UCollisionProfile::Pawn_ProfileName);
  GetStaticMeshComponent()->bGenerateOverlapEvents = true;
}
GetStaticMeshComponent()->SetMobility(EComponentMobility::Movable);
GetStaticMeshComponent()-> SetWorldScale3D(FVector(0.5, 0.5, 0.5));
SetActorEnableCollision(true);

DoorToOpen = nullptr;
  1. 将以下函数实现添加到我们的DoorBell上以实现Interactable接口:
boolADoorBell::CanInteract_Implementation()
{
  return !HasBeenPushed;
}

voidADoorBell::PerformInteract_Implementation()
{
  HasBeenPushed = true;
  if (DoorToOpen->GetClass()->ImplementsInterface(UOpenable::StaticClass()))
  {
    IOpenable::Execute_Open(DoorToOpen);
  }
}
  1. 现在创建一个基于StaticMeshActor的新类,名为Door

  2. 在类头文件中#include OpenableInteractable接口,然后修改Door的声明:

class UE4COOKBOOK_API ADoor : public AStaticMeshActor, public IInteractable, public IOpenable
  1. 将接口函数添加到Door上:
UFUNCTION()
virtual bool CanInteract_Implementation() override { return IInteractable::CanInteract_Implementation(); };
UFUNCTION()
virtual void PerformInteract_Implementation() override;

UFUNCTION()
virtual void Open_Implementation() override;
  1. DoorBell一样,在Door构造函数中,初始化我们的网格组件,并加载一个模型:
autoMeshAsset = ConstructorHelpers::FObjectFinder<UStaticMesh>(TEXT("StaticMesh'/Engine/BasicShapes/Cube.Cube'"));
if (MeshAsset.Object != nullptr)
{
  GetStaticMeshComponent()->SetStaticMesh(MeshAsset.Object);
  //GetStaticMeshComponent()->SetCollisionProfileName(UCollisionProfile::Pawn_ProfileName);
  GetStaticMeshComponent()->bGenerateOverlapEvents = true;
}
GetStaticMeshComponent()->SetMobility(EComponentMobility::Movable);
GetStaticMeshComponent()->SetWorldScale3D(FVector(0.3, 2, 3));
SetActorEnableCollision(true);
  1. 实现接口函数:
voidADoor::PerformInteract_Implementation()
{
  GEngine->AddOnScreenDebugMessage(-1, 5, FColor::Red, TEXT("The door refuses to budge. Perhaps there is a hidden switch nearby?"));
}

voidADoor::Open_Implementation()
{
  AddActorLocalOffset(FVector(0, 0, 200));
}
  1. 创建一个基于DefaultPawn的新类,名为AInteractingPawn

  2. 将以下函数添加到Pawn类头文件中:

voidTryInteract();

private:
virtual void SetupPlayerInputComponent(UInputComponent* InInputComponent) override;
  1. Pawn的实现文件中,#include "Interactable.h",然后为头文件中的两个函数提供实现:
voidAInteractingPawn::TryInteract()
{
  APlayerController* MyController = Cast<APlayerController>(Controller);
  if (MyController)
  {
    APlayerCameraManager* MyCameraManager = MyController->PlayerCameraManager;
    autoStartLocation = MyCameraManager->GetCameraLocation();
    autoEndLocation = MyCameraManager->GetCameraLocation() + (MyCameraManager->GetActorForwardVector() * 100);
    FHitResultHitResult;
    GetWorld()->SweepSingleByObjectType(HitResult, StartLocation, EndLocation, FQuat::Identity, 
    FCollisionObjectQueryParams(FCollisionObjectQueryParams::AllObjects),FCollisionShape::MakeSphere(25),
    FCollisionQueryParams(FName("Interaction"),true,this));
    if (HitResult.Actor != nullptr)
    {
      if (HitResult.Actor->GetClass()->ImplementsInterface(UInteractable::StaticClass()))
      {
        if (IInteractable::Execute_CanInteract(HitResult.Actor.Get()))
        {
          IInteractable::Execute_PerformInteract(HitResult.Actor.Get());
        }
      }
    }
  }
}
voidAInteractingPawn::SetupPlayerInputComponent(UInputComponent* InInputComponent)
{
  Super::SetupPlayerInputComponent(InInputComponent);
  InInputComponent->BindAction("Interact", IE_Released, this, &AInteractingPawn::TryInteract);
}
  1. 现在,要么在 C++中创建一个新的GameMode,要么在蓝图中创建一个新的GameMode,并将InteractingPawn设置为我们的默认Pawn类。

  2. DoorDoorbell的副本拖到级别中:如何操作...

  3. 使用眼滴工具在门铃的Door to Open旁边,如下图所示,然后单击您级别中的门角色实例:如何操作...如何操作...

  4. 在编辑器中创建一个名为Interact的新动作绑定,并将其绑定到您选择的一个键:如何操作...

  5. 播放您的级别,并走到门铃旁。看着它,按下您绑定Interact的键。验证门是否移动一次。

  6. 您还可以直接与门交互以获取有关它的一些信息。

它是如何工作的...

  1. 与以前的教程一样,我们将UFUNCTION标记为BlueprintNativeEventBlueprintCallable,以允许UInterface在本地代码或蓝图中实现,并允许使用任一方法调用函数。

  2. 我们基于StaticMeshActor创建DoorBell以方便起见,并使DoorBell实现Interactable接口。

  3. DoorBell的构造函数中,我们将HasBeenPushedDoorToOpen初始化为默认安全值。

  4. CanInteract的实现中,我们返回HasBeenPushed的反值,以便一旦按钮被按下,就无法进行交互。

  5. PerformInteract中,我们检查是否有一个引用来打开门对象。

  6. 如果我们有一个有效的引用,我们验证门角色是否实现了Openable,然后在我们的门上调用Open函数。

  7. Door中,我们实现了InteractableOpenable,并重写了每个函数。

  8. 我们将DoorCanInteract实现定义为与默认值相同。

  9. PerformInteract中,我们向用户显示一条消息。

  10. Open函数中,我们使用AddActorLocalOffset来将门移动到一定的距离。通过蓝图中的时间轴或线性插值,我们可以使这个过渡变得平滑,而不是瞬间移动。

  11. 最后,我们创建一个新的Pawn,以便玩家实际上可以与物体交互。

  12. 我们创建一个TryInteract函数,并将其绑定到重写的SetupPlayerInputComponent函数中的Interact输入动作。

  13. 这意味着当玩家执行与Interact绑定的输入时,我们的TryInteract函数将运行。

  14. TryInteract获取对PlayerController的引用,将所有 Pawns 都具有的通用控制器引用进行转换。

  15. 通过PlayerController检索PlayerCameraManager,这样我们就可以访问玩家摄像机的当前位置和旋转。

  16. 我们使用摄像机的位置创建起始点和结束点,然后在摄像机位置的前方 100 个单位处,将它们传递给GetWorld::SweepSingleByObjectType函数。

  17. 这个函数接受多个参数。HitResult是一个变量,允许函数返回有关跟踪到的任何对象的信息。CollisionObjectQueryParams允许我们指定我们是否对动态、静态物品或两者都感兴趣。

  18. 我们通过使用MakeSphere函数来完成一个球体跟踪。

  19. 球体跟踪通过定义一个圆柱体来检查物体,而不是一条直线,从而允许稍微有些人为误差。考虑到玩家可能不会完全准确地看着你的物体,你可以根据需要调整球体的半径。

  20. 最后一个参数SweepSingleByObjectType是一个结构体,它给跟踪一个名称,让我们指定是否与复杂的碰撞几何体发生碰撞,最重要的是,它允许我们指定我们要忽略发起跟踪的对象。

  21. 如果HitResult在跟踪完成后包含一个 actor,我们检查该 actor 是否实现了我们的接口,然后尝试调用CanInteract函数。

  22. 如果 actor 表示可以进行交互,我们就告诉它实际执行交互操作。

第八章:集成 C++和虚幻编辑器

在本章中,我们将介绍以下内容:

  • 使用类或结构作为蓝图变量

  • 创建可以在蓝图中作为子类化的类或结构

  • 创建可以在蓝图中调用的函数

  • 创建可以在蓝图中实现的事件

  • 将多播委托公开给蓝图

  • 创建可以在蓝图中使用的 C++枚举

  • 在编辑器中的不同位置编辑类属性

  • 使属性在蓝图编辑器图中可访问

  • 响应编辑器中的属性更改事件

  • 实现本地代码构造脚本

  • 创建一个新的编辑器模块

  • 创建新的工具栏按钮

  • 创建新的菜单项

  • 创建一个新的编辑器窗口

  • 创建一个新的资产类型

  • 为资产创建自定义上下文菜单项

  • 创建新的控制台命令

  • 为蓝图创建一个新的图钉可视化器

  • 使用自定义详细信息面板检查类型

介绍

虚幻的主要优势之一是它为程序员提供了创建可以由设计师在编辑器中自定义或使用的角色和其他对象的能力。本章展示了如何实现这一点。在此之后,我们将尝试通过从头开始创建自定义蓝图和动画节点来自定义编辑器。我们还将实现自定义编辑器窗口和用于检查用户创建的类型的自定义详细信息面板。

使用类或结构作为蓝图变量

在 C++中声明的类型不会自动并入蓝图以用作变量。此示例向您展示如何使它们可访问,以便您可以将自定义本地代码类型用作蓝图函数参数。

如何操作...

  1. 使用编辑器创建一个新的类。与之前的章节不同,我们将创建一个基于对象的类。对象在常见类列表中不可见,因此我们需要在编辑器 UI 中选中“显示所有类”按钮,然后选择“对象”。将您的新“对象”子类命名为TileType如何操作...

  2. 将以下属性添加到TileType定义中:

UPROPERTY()
int32 MovementCost;
UPROPERTY()
bool CanBeBuiltOn;

UPROPERTY()
FString TileName;
  1. 编译您的代码。

  2. 在编辑器中,基于Actor创建一个新的蓝图类。将其命名为Tile如何操作...

  3. Tile的蓝图编辑器中,向蓝图添加一个新变量。检查您可以创建为变量的类型列表,并验证TileType是否不在其中。如何操作...

  4. BlueprintType添加到UCLASS宏中,如下所示:

UCLASS(BlueprintType)
class UE4COOKBOOK_API UTileType : public UObject
{
}
  1. 重新编译项目,然后返回到Tile蓝图编辑器。

  2. 现在,当您向角色添加新变量时,可以选择TileType作为新变量的类型。如何操作...

  3. 我们现在已经建立了TileTileType之间的“有一个”关系。

  4. 现在,TileType是一个可以用作函数参数的蓝图类型。在您的Tile蓝图上创建一个名为SetTileType的新函数。如何操作...

  5. 添加一个新的输入:如何操作...

  6. 将输入参数的类型设置为TileType如何操作...

  7. 您可以将我们的Type变量拖动到视口中,并选择“设置”。如何操作...

  8. SetTileTypeExec引脚和输入参数连接到Set节点。如何操作...

它是如何工作的...

  1. 出于性能原因,虚幻假设类不需要额外的反射代码,以使类型可用于蓝图。

  2. 我们可以通过在我们的UCLASS宏中指定BlueprintType来覆盖此默认值。

  3. 包含说明符后,该类型现在可以作为蓝图中的参数或变量使用。

还有更多...

此示例显示,如果其本地代码声明包括BlueprintType,则可以在蓝图中使用类型作为函数参数。

然而,目前我们在 C++中定义的属性都无法在蓝图中访问。

本章的其他示例涉及使这些属性可访问,以便我们可以对自定义对象进行有意义的操作。

创建可以在蓝图中进行子类化的类或结构体

虽然本书侧重于 C++,但在使用虚幻引擎进行开发时,更标准的工作流程是将核心游戏功能以及性能关键代码实现为 C++,并将这些功能暴露给蓝图,以便设计师可以原型化游戏玩法,然后由程序员使用额外的蓝图功能进行重构,或者将其推回到 C++层。

其中一个最常见的任务是以这样的方式标记我们的类和结构体,以便它们对蓝图系统可见。

如何做...

  1. 使用编辑器向导创建一个新的Actor类;将其命名为BaseEnemy

  2. 将以下UPROPERTY添加到该类中:

UPROPERTY()
FString WeaponName;
UPROPERTY()
int32 MaximumHealth;
  1. 将以下类限定符添加到UCLASS宏中:
UCLASS(Blueprintable)
class UE4COOKBOOK_API ABaseEnemy : public AActor
  1. 打开编辑器并创建一个新的蓝图类。展开列表以显示所有类,并选择我们的BaseEnemyclass作为父类。如何做...

  2. 将新的蓝图命名为EnemyGoblin并在蓝图编辑器中打开它。

  3. 请注意,我们之前创建的UPROPERTY宏还不存在,因为我们尚未包含适当的标记以使它们对蓝图可见。

它是如何工作的...

  1. 前面的示例演示了BlueprintType作为类限定符的用法。BlueprintType允许将该类型用作蓝图编辑器中的类型(即,它可以是变量或函数的输入/返回值)。

  2. 然而,我们可能希望基于我们的类型创建蓝图(使用继承),而不是组合(例如将我们的类型的实例放在Actor内部)。

  3. 这就是为什么 Epic 提供了Blueprintable作为类限定符的原因。Blueprintable意味着开发人员可以将类标记为蓝图类的可继承类。

  4. 我们使用了BlueprintTypeBlueprintable而不是单个组合限定词,因为有时您可能只需要部分功能。例如,某些类应该可用作变量,但出于性能原因,不允许在蓝图中创建它们。在这种情况下,您将使用BlueprintType而不是两个限定词。

  5. 另一方面,也许我们想要使用蓝图编辑器创建新的子类,但我们不想在Actor蓝图中传递对象实例。在这种情况下,建议使用Blueprintable,但在这种情况下省略BlueprintType

  6. 与之前一样,BlueprintableBlueprintType都没有指定类中包含的成员函数或成员变量的任何信息。我们将在后面的示例中使它们可用。

创建可以在蓝图中调用的函数

将类标记为BlueprintTypeBlueprintable允许我们在蓝图中传递类的实例,或者用蓝图类对类型进行子类化,但这些限定词实际上并不涉及成员函数或变量,以及它们是否应该暴露给蓝图。

本示例向您展示了如何标记函数,以便可以在蓝图图表中调用它。

如何做...

  1. 使用编辑器创建一个新的Actor类。将该 actor 命名为SlidingDoor

  2. 将以下UPROPERTY添加到新类中:

UFUNCTION(BlueprintCallable, Category = Door)
void Open();
UPROPERTY()
bool IsOpen;

UPROPERTY()
FVector TargetLocation;
  1. 通过将以下内容添加到.cpp文件中来创建类的实现:
ASlidingDoor::ASlidingDoor()
:Super()
{
  auto MeshAsset = ConstructorHelpers::FObjectFinder<UStaticMesh>(TEXT("StaticMesh'/Engine/BasicShapes/Cube.Cube'"));
  if (MeshAsset.Object != nullptr)
  {
    GetStaticMeshComponent()->SetStaticMesh(MeshAsset.Object);
    GetStaticMeshComponent()->bGenerateOverlapEvents = true;
  }
  GetStaticMeshComponent()->SetMobility(EComponentMobility::Movable);
  GetStaticMeshComponent()->SetWorldScale3D(FVector(0.3, 2, 3));
  SetActorEnableCollision(true);
  IsOpen = false;
  PrimaryActorTick.bStartWithTickEnabled = true;
  PrimaryActorTick.bCanEverTick = true;
}
void ASlidingDoor::Open()
{
  TargetLocation = ActorToWorld().TransformPositionNoScale(FVector(0, 0, 200));
  IsOpen = true;
}

void ASlidingDoor::Tick(float DeltaSeconds)
{
  if (IsOpen)
  {
    SetActorLocation(FMath::Lerp(GetActorLocation(), TargetLocation, 0.05));
  }
}
  1. 编译代码并启动编辑器。

  2. 将门的副本拖动到关卡中。

  3. 确保选择了SlidingDoor实例,然后打开关卡蓝图。右键单击空白画布,展开在 Sliding Door 1 上调用函数如何做...

  4. 展开Door部分,然后选择Open函数。如何做...

  5. 将执行引脚(白色箭头)从BeginPlay连接到Open节点上的白色箭头,如下图所示:如何做...

  6. 播放您的关卡,并验证当在门实例上调用Open时,门是否按预期移动。如何操作...

它的工作原理是...

  1. 在门的声明中,我们创建一个新的函数来打开门,一个布尔值来跟踪门是否已被告知打开,以及一个向量,允许我们预先计算门的目标位置。

  2. 我们还重写了Tick actor 函数,以便我们可以在每一帧上执行一些行为。

  3. 在构造函数中,我们加载立方体网格并缩放它以表示我们的门。

  4. 我们还将IsOpen设置为已知的好值false,并通过使用bCanEverTickbStartWithTickEnabled启用 actor ticking。

  5. 这两个布尔值分别控制是否可以为此 actor 启用 ticking 以及是否以启用状态开始 ticking。

  6. Open函数内部,我们计算相对于门的起始位置的目标位置。

  7. 我们还将IsOpen布尔值从false更改为true

  8. 现在IsOpen布尔值为true,在Tick函数内部,门尝试使用SetActorLocationLerp将自身移动到目标位置,以在当前位置和目标位置之间进行插值。

另请参阅

  • 第五章, 处理事件和委托,有一些与生成 actor 相关的示例

创建可以在蓝图中实现的事件

C++与 Blueprint 更紧密集成的另一种方式是创建可以在本地代码中具有 Blueprint 实现的函数。这允许程序员指定一个事件并调用它,而无需了解任何实现细节。然后可以在 Blueprint 中对该类进行子类化,并且制作团队的另一成员可以实现该事件的处理程序,而无需接触任何 C++代码。

如何操作...

  1. 创建一个名为Spotter的新StaticMeshActor类。

  2. 确保在类头文件中定义并重写以下函数:

virtual void Tick( float DeltaSeconds ) override;
UFUNCTION(BlueprintImplementableEvent)
void OnPlayerSpotted(APawn* Player);
  1. 将此代码添加到构造函数中:
PrimaryActorTick.bCanEverTick = true;
auto MeshAsset = ConstructorHelpers::FObjectFinder<UStaticMesh>(TEXT("StaticMesh'/Engine/BasicShapes/Cone.Cone'"));
if (MeshAsset.Object != nullptr)
{
  GetStaticMeshComponent()->SetStaticMesh(MeshAsset.Object);
  GetStaticMeshComponent()->bGenerateOverlapEvents = true;
}
GetStaticMeshComponent()->SetMobility(EComponentMobility::Movable);
GetStaticMeshComponent()->SetRelativeRotation(FRotator(90, 0, 0));
  1. 将此代码添加到Tick函数中:
Super::Tick( DeltaTime );

auto EndLocation = GetActorLocation() + ActorToWorld().TransformVector(FVector(0,0,-200));
FHitResult HitResult;
GetWorld()->SweepSingleByChannel(HitResult, GetActorLocation(), EndLocation, FQuat::Identity, ECC_Camera, FCollisionShape::MakeSphere(25), FCollisionQueryParams("Spot", true, this));
APawn* SpottedPlayer = Cast<APawn>(HitResult.Actor.Get());

if (SpottedPlayer!= nullptr)
{
  OnPlayerSpotted(SpottedPlayer);
}
DrawDebugLine(GetWorld(), GetActorLocation(), EndLocation, FColor::Red);
  1. 编译并启动编辑器。在内容浏览器中找到您的Spotter类,然后左键单击并将其拖到游戏世界中。

  2. 当您播放关卡时,您将看到红线显示Actor执行的追踪。但是,什么都不会发生,因为我们还没有实现我们的OnPlayerSpotted事件。

  3. 为了实现这个事件,我们需要创建一个Spotter的蓝图子类。

  4. 内容浏览器中右键单击Spotter,然后选择基于 Spotter 创建蓝图类。将类命名为BPSpotter如何操作...

  5. 在蓝图编辑器中,点击My Blueprint面板的Functions部分的Override按钮:如何操作...

  6. 选择On Player Spotted如何操作...

  7. 从我们的事件的白色执行引脚上左键单击并拖动。在出现的上下文菜单中,选择并添加一个Print String节点,以便它与事件链接起来。如何操作...

  8. 再次播放关卡,并验证走在Spotter正在使用的追踪前是否将字符串打印到屏幕上。如何操作...

它的工作原理是...

  1. 在我们的Spotter对象的构造函数中,我们将一个基本的原始体,一个锥体,加载到我们的静态网格组件中作为视觉表示。

  2. 然后,我们旋转锥体,使其类似于指向 actor 的X轴的聚光灯。

  3. Tick函数期间,我们获取 actor 的位置,然后找到沿其本地X轴 200 个单位的点。我们使用Super::调用父类的Tick实现,以确保尽管我们进行了重写,但任何其他 tick 功能都得以保留。

  4. 通过首先获取Actor的 Actor-to-World 变换,然后使用该变换来转换指定位置的向量,将局部位置转换为世界空间位置。

  5. 变换基于根组件的方向,根组件是我们在构造函数中旋转的静态网格组件。

  6. 由于现有的旋转,我们需要旋转我们想要转换的向量。考虑到我们希望向量指向锥体底部的方向,我们希望沿着负上轴的距离,也就是说,我们希望一个形如(0,0,-d)的向量,其中d是实际的距离。

  7. 计算了我们追踪的最终位置后,我们实际上使用SweepSingleByChannel函数进行追踪。

  8. 执行扫描后,我们尝试将结果命中的Actor转换为一个 pawn。

  9. 如果转换成功,我们调用OnPlayerSpotted的可实现事件,并执行用户定义的蓝图代码。

将多播委托公开给蓝图

多播委托是一种将事件广播给多个监听订阅该事件的对象的好方法。如果你有一个生成事件的 C++模块,可能会有任意的 Actor 想要被通知到这些事件,那么多播委托尤其有价值。本示例向你展示了如何在 C++中创建一个多播委托,以便在运行时通知一组其他 Actor。

如何操作...

  1. 创建一个名为King的新的StaticMeshActor类。在类头文件中添加以下内容:
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnKingDeathSignature, AKing*, DeadKing);
  1. 在类中添加一个新的UFUNCTION
UFUNCTION(BlueprintCallable, Category = King)
void Die();
  1. 向类中添加我们的多播委托的实例:
UPROPERTY(BlueprintAssignable)
FOnKingDeathSignature OnKingDeath;
  1. 将我们的网格初始化添加到构造函数中:
auto MeshAsset = ConstructorHelpers::FObjectFinder<UStaticMesh>(TEXT("StaticMesh'/Engine/BasicShapes/Cone.Cone'"));
if (MeshAsset.Object != nullptr)
{
  GetStaticMeshComponent()->SetStaticMesh(MeshAsset.Object);
  GetStaticMeshComponent()->bGenerateOverlapEvents = true;
}
GetStaticMeshComponent()->SetMobility(EComponentMobility::Movable);
  1. 实现Die函数:
void AKing::Die()
{
  OnKingDeath.Broadcast(this);
}
  1. 创建一个名为Peasant的新类,也是基于StaticMeshActor的。

  2. 在类中声明一个默认构造函数:

APeasant();
  1. 声明以下函数:
UFUNCTION(BlueprintCallable, category = Peasant)
void Flee(AKing* DeadKing);
  1. 实现构造函数:
auto MeshAsset = ConstructorHelpers::FObjectFinder<UStaticMesh>(TEXT("StaticMesh'/Engine/BasicShapes/Cube.Cube'"));
if (MeshAsset.Object != nullptr)
{
  GetStaticMeshComponent()->SetStaticMesh(MeshAsset.Object);
  GetStaticMeshComponent()->bGenerateOverlapEvents = true;
}
GetStaticMeshComponent()->SetMobility(EComponentMobility::Movable);
  1. .cpp文件中实现该函数:
void APeasant::Flee(AKing* DeadKing)
{
  GEngine->AddOnScreenDebugMessage(-1, 2, FColor::Red, TEXT("Waily Waily!"));
  FVector FleeVector = GetActorLocation() – DeadKing->GetActorLocation();
  FleeVector.Normalize();
  FleeVector *= 500;
  SetActorLocation(GetActorLocation() + FleeVector);
}
  1. 打开蓝图并创建一个基于APeasant的蓝图类,命名为BPPeasant

  2. 在蓝图中,点击并拖动离你的BeginPlay节点的白色(执行)引脚。输入get all,你应该会看到获取所有类的 Actor。选择该节点并放置在你的图表中。如何操作...

  3. 将紫色(类)节点的值设置为King。你可以在搜索栏中输入king以更容易地找到该类。如何操作...

  4. 从蓝色网格(对象数组)节点拖动到空白处并放置一个获取节点。如何操作...

  5. 从获取节点的蓝色输出引脚处拖动,并放置一个不等(对象)节点。如何操作...

  6. 将不等(bool)节点的红色引脚连接到一个Branch节点,并将Branch节点的执行引脚连接到我们的Get All Actors Of Class节点。如何操作...

  7. 将分支的True引脚连接到Bind Event to OnKing Death节点。如何操作...

注意

注意,你可能需要在上下文菜单中取消选中上下文敏感以使绑定事件节点可见。

  1. Bind Event节点的红色引脚拖动,并在释放鼠标左键后出现的上下文菜单中选择Add Custom Event…如何操作...

  2. 给你的事件命名,然后将白色执行引脚连接到一个名为Flee的新节点。如何操作...

  3. 验证你的蓝图是否如下图所示:如何操作...

  4. 将你的King类的副本拖动到关卡中,然后在其周围以圆形添加几个BPPeasant实例。

  5. 打开关卡蓝图。在其中,从BeginPlay处拖动并添加一个Delay节点。将延迟设置为5秒。如何操作...

  6. 在关卡中选择你的King实例,在图形编辑器中右键单击。

  7. 选择Call function on King 1,并在King类别中查找一个名为Die的函数。如何操作...

  8. 选择Die,然后将其执行引脚连接到延迟的输出执行引脚。如何做...

  9. 当你播放关卡时,你应该会看到国王在 5 秒后死亡,农民们都哀号并直接远离国王。如何做...如何做...

工作原理...

  1. 我们创建一个新的演员(基于StaticMeshActor方便起见,因为这样可以省去为Actor的可视化表示声明或创建静态网格组件的步骤)。

  2. 我们使用DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam宏声明了一个动态多播委托。动态多播委托允许任意数量的对象订阅(监听)和取消订阅(停止监听),以便在广播委托时通知它们。

  3. 该宏接受多个参数-正在创建的新委托签名的类型名称,签名参数的类型,然后是签名参数的名称。

  4. 我们还在King中添加了一个函数,允许我们告诉它死亡。因为我们希望将该函数暴露给蓝图进行原型设计,所以将其标记为BlueprintCallable

  5. 我们之前使用的DECLARE_DYNAMIC_MULTICAST_DELEGATE宏只声明了一个类型,没有声明委托的实例,所以现在我们要做的是声明一个委托的实例,引用之前在调用宏时提供的类型名称。

  6. 动态多播委托可以在其UPROPERTY声明中标记为BlueprintAssignable。这告诉虚幻引擎蓝图系统可以动态地将事件分配给委托,当调用委托的Broadcast函数时将调用这些事件。

  7. 像往常一样,我们为我们的King分配一个简单的网格,以便在游戏场景中有一个可视化表示。

  8. Die函数内部,我们调用自己的委托上的Broadcast函数。我们指定委托将有一个指向死去的国王的指针作为参数,所以我们将这个指针作为参数传递给广播函数。

注意

如果你希望国王被销毁,而不是在死亡时播放动画或其他效果,你需要改变委托的声明并传入不同的类型。例如,你可以使用FVector,并直接传入死去的国王的位置,这样农民仍然可以适当地逃离。

如果没有这个,当调用Broadcast时,King指针可能是有效的,但在执行绑定函数之前,调用Actor::Destroy()会使其无效。

  1. 在我们的下一个StaticMeshActor子类Peasant中,我们像往常一样初始化静态网格组件,使用了与King不同的形状。

  2. 在农民的Flee函数的实现中,我们通过在屏幕上打印一条消息来模拟农民发出声音。

  3. 然后,我们计算一个向量,首先找到从死去的国王到这个农民位置的向量。

  4. 我们将向量归一化以获得指向相同方向的单位向量(长度为 1)。

  5. 通过缩放归一化向量并将其添加到当前位置,可以计算出一个固定距离的位置,正好是农民直接远离死去的国王的方向。

  6. 然后使用SetActorLocation来实际将农民传送到该位置。

注意

如果你使用带有 AI 控制器的角色,你可以让Peasant寻路到目标位置,而不是瞬间传送。或者,你可以在农民的Tick中使用Lerp函数来使它们平滑滑动,而不是直接跳到位置。

另请参阅

  • 在第四章中查看有关演员和组件的更详细讨论,第五章中讨论了诸如NotifyActorOverlap之类的事件。

创建可以在蓝图中使用的 C++枚举

枚举通常在 C++中用作标志或输入到 switch 语句中。然而,如果你想要从蓝图向 C++传递一个“枚举”值,或者从 C++向蓝图传递一个“枚举”值,该怎么办?或者,如果你想在蓝图中使用一个使用 C++中的“枚举”的switch语句,你如何让蓝图编辑器知道你的“枚举”应该在编辑器中可访问?本教程向你展示了如何使枚举在蓝图中可见。

如何操作...

  1. 使用编辑器创建一个名为Tree的新的StaticMeshActor类。

  2. 在类声明之前插入以下代码:

UENUM(BlueprintType)
enum TreeType
{
  Tree_Poplar,
  Tree_Spruce,
  Tree_Eucalyptus,
  Tree_Redwood
};
  1. Tree类中添加以下UPROPERTY
UPROPERTY(BlueprintReadWrite)
TEnumAsByte<TreeType> Type;
  1. Tree构造函数中添加以下内容:
auto MeshAsset = ConstructorHelpers::FObjectFinder<UStaticMesh>(TEXT("StaticMesh'/Engine/BasicShapes/Cylinder.Cylinder'"));
if (MeshAsset.Object != nullptr)
{
  GetStaticMeshComponent()->SetStaticMesh(MeshAsset.Object);
  GetStaticMeshComponent()->bGenerateOverlapEvents = true;
}
GetStaticMeshComponent()->SetMobility(EComponentMobility::Movable);
  1. 创建一个名为MyTree的新蓝图类,基于Tree

  2. MyTree的蓝图编辑器中,点击“构造脚本”选项卡。

  3. 在空白窗口中右键点击,输入treetype。有一个“获取 TreeType 中的条目数”节点。如何操作...

  4. 放置它,然后将其输出引脚连接到一个“随机整数”节点。如何操作...

  5. 将随机整数的输出连接到ToByte节点的输入。如何操作...

  6. 在蓝图面板的“变量”部分,展开“Tree”并选择“Type”。如何操作...

  7. 将其拖入图中,并在出现小的上下文菜单时选择“Set”。

  8. ToByte节点的输出连接到“SET 类型”节点的输入。你会看到一个额外的转换节点自动出现。如何操作...

  9. 最后,将“构造脚本”的执行引脚连接到“SET 类型”节点的执行引脚。

  10. 你的蓝图应该如下所示:如何操作...

  11. 为了验证蓝图是否正确运行并随机分配类型给我们的树,我们将在事件图中添加一些节点。

  12. 在“Event BeginPlay”事件节点之后放置一个“打印字符串”节点。如何操作...

  13. 放置一个“格式文本”节点,并将其输出连接到“打印字符串”节点的输入。一个转换节点将会被添加给你。如何操作...

  14. 在“格式文本”节点中,将“My Type is {0}!”添加到文本框中。如何操作...

  15. 从蓝图的变量部分拖动Type到图中,从菜单中选择“Get”。如何操作...

  16. 将一个“Enum to Name”节点添加到Type的输出引脚。如何操作...

  17. 将名称输出连接到标记为0的“格式文本”的输入引脚。如何操作...

  18. 你的事件图现在应该如下所示:如何操作...

  19. 将几个副本的蓝图拖入关卡并点击“播放”。你应该看到一些树打印有关它们类型的信息,验证了我们创建的蓝图代码随机分配类型的功能。如何操作...

工作原理...

  1. 和往常一样,我们使用StaticMeshActor作为我们的Actor的基类,以便我们可以在关卡中轻松地给它一个可视化表示。

  2. 使用UENUM宏将枚举类型暴露给反射系统。

  3. 我们使用BlueprintType修饰符将“枚举”标记为蓝图可用。

  4. “枚举”声明与我们在任何其他上下文中使用的方式完全相同。

  5. 我们的Tree需要一个TreeType。因为我们想要体现的关系是树具有树类型,所以我们在Tree类中包含了一个TreeType的实例。

  6. 和往常一样,我们需要使用UPROPERTY()使成员变量对反射系统可访问。

  7. 我们使用BlueprintReadWrite修饰符来标记该属性在蓝图中具有获取和设置的支持。

  8. 当在UPROPERTY中使用时,枚举类型需要被包装在TEnumAsByte模板中,因此我们声明一个TEnumAsByte<TreeType>的实例作为树的Type变量。

  9. Tree的构造函数只是标准的加载和初始化我们在其他示例中使用的静态网格组件前导。

  10. 我们创建一个继承自我们的Tree类的蓝图,以便我们可以演示TreeType enum的蓝图可访问性。

  11. 为了使蓝图在创建实例时随机分配树的类型,我们需要使用蓝图的Construction Script

  12. Construction Script中,我们计算TreeType enum中的条目数。

  13. 我们生成一个随机数,并将其作为TreeType enum类型中的索引来检索一个值,将其存储为我们的Type

  14. 然而,随机数节点返回整数。在蓝图中,枚举类型被视为字节,因此我们需要使用ToByte节点,然后蓝图可以将其隐式转换为enum值。

  15. 现在,我们已经在Construction Script中为创建的树实例分配了一个类型,我们需要在运行时显示树的类型。

  16. 我们通过连接到事件图表选项卡中的BeginPlay事件附加的图表来实现。

  17. 要在屏幕上显示文本,我们使用Print String节点。

  18. 为了执行字符串替换并将我们的类型打印为可读字符串,我们使用Format Text节点。

  19. Format Text节点接受用花括号括起来的术语,并允许您替换这些术语的其他值,返回最终的字符串。

  20. 将我们的Type替换到Format Text节点中,我们需要将变量存储从enum值转换为实际值的名称。

  21. 我们可以通过访问我们的Type变量,然后使用Enum to Name节点来实现。

  22. Name或本机代码中的FNames是一种可以由蓝图转换为字符串的变量类型,因此我们可以将我们的Name连接到Format Text节点的输入上。

  23. 当我们点击播放时,图表执行,检索放置在关卡中的树实例的类型,并将名称打印到屏幕上。

在编辑器中的不同位置编辑类属性

在使用虚幻引擎进行开发时,程序员通常会在 C++中为 Actor 或其他对象实现属性,并使其对设计师可见。然而,有时候查看属性或使其可编辑是有意义的,但仅在对象的默认状态下。有时,属性只能在运行时进行修改,其默认值在 C++中指定。幸运的是,有一些修饰符可以帮助我们限制属性的可用性。

如何操作...

  1. 在编辑器中创建一个名为PropertySpecifierActor的新Actor类。

  2. 将以下属性定义添加到类中:

UPROPERTY(EditDefaultsOnly)
bool EditDefaultsOnly;
UPROPERTY(EditInstanceOnly)
bool EditInstanceOnly;
UPROPERTY(EditAnywhere)
bool EditAnywhere;
UPROPERTY(VisibleDefaultsOnly)
bool VisibleDefaultsOnly;
UPROPERTY(VisibleInstanceOnly)
bool VisibleInstanceOnly;
UPROPERTY(VisibleAnywhere)
bool VisibleAnywhere;
  1. 编译代码并启动编辑器。

  2. 在编辑器中基于该类创建一个新的蓝图。

  3. 打开蓝图,查看Class Defaults部分。如何操作...

  4. 请注意哪些属性是可编辑和可见的。如何操作...

  5. 在关卡中放置实例,并查看它们的Details面板。如何操作...

  6. 请注意,可编辑的属性集不同。

它是如何工作的...

  1. 在指定UPROPERTY时,我们可以指示我们希望该值在虚幻编辑器中的哪个位置可用。

  2. Visible*前缀表示该值可以在指定对象的Details面板中查看。但是,该值不可编辑。

  3. 这并不意味着变量是const限定符;然而,本机代码可以更改值,例如。

  4. Edit*前缀表示该属性可以在编辑器中的Details面板中进行更改。

  5. 作为后缀的InstanceOnly表示该属性仅在已放置到游戏中的类的实例的“详细信息”面板中显示。例如,在蓝图编辑器的“类默认”部分中将不可见。

  6. DefaultsOnlyInstanceOnly的反义词 - UPROPERTY仅显示在“类默认部分”中,并且无法在蓝图编辑器中的单个实例上查看。

  7. 后缀Anywhere是前两个后缀的组合 - UPROPERTY将在检查对象的默认值或级别中的特定实例的所有“详细信息”面板中可见。

另请参阅

  • 这个配方使得所讨论的属性在检视器中可见,但不允许在实际的蓝图事件图中引用该属性。请参阅下一个配方,了解如何实现这一点的描述。

使属性在蓝图编辑器图中可访问

前一个配方中提到的限定词都很好,但它们只控制了UPROPERTY在“详细信息”面板中的可见性。默认情况下,即使适当使用这些限定词,也无法在实际的编辑器图中查看或访问UPROPERTY以供运行时使用。

其他限定词可以选择与前一个配方中的限定词一起使用,以允许在事件图中与属性交互。

操作方法…

  1. 使用编辑器向导创建一个名为BlueprintPropertyActor的新Actor类。

  2. 使用 Visual Studio 将以下UPROPERTY添加到 Actor 中:

UPROPERTY(BlueprintReadWrite, Category = Cookbook)
bool ReadWriteProperty;
UPROPERTY(BlueprintReadOnly, Category = Cookbook)
bool ReadOnlyProperty;
  1. 编译项目并启动编辑器。

  2. 创建一个基于你的“BlueprintPropertyActor”的蓝图类,并打开其图表。

  3. 验证属性在“我的蓝图”面板的“变量”部分下的“Cookbook”类别下可见。操作方法…

  4. 左键单击并将 ReadWrite 属性拖入事件图中,然后选择Get操作方法…

  5. 重复上一步并选择Set

  6. ReadOnly属性拖入图表中,并注意Set节点被禁用。操作方法…

工作原理…

  1. 作为UPROPERTY限定词的BlueprintReadWrite指示虚幻头文件工具应为蓝图公开该属性的GetSet操作。

  2. BlueprintReadOnly是一个只允许蓝图检索属性值而不允许设置的限定词。

  3. 当属性由本地代码设置但应在蓝图中访问时,BlueprintReadOnly非常有用。

  4. 应该注意的是,BlueprintReadWriteBlueprintReadOnly并没有指定属性在“详细信息”面板或编辑器的“我的蓝图”部分中是否可访问 - 这些限定词只控制用于蓝图图表中的 getter/setter 节点的生成。

响应编辑器中属性更改事件

当设计师更改放置在级别中的Actor的属性时,通常重要的是立即显示该更改的任何视觉结果,而不仅仅在模拟或播放级别时显示。

当使用“详细信息”面板进行更改时,编辑器会发出一个特殊事件,称为PostEditChangeProperty,该事件使类实例有机会响应属性的编辑。

本配方向您展示如何处理PostEditChangeProperty以实现即时的编辑器反馈。

操作方法…

  1. 基于StaticMeshActor创建一个名为APostEditChangePropertyActor的新Actor

  2. 将以下UPROPERTY添加到类中:

UPROPERTY(EditAnywhere)
bool ShowStaticMesh;
  1. 添加以下函数定义:
virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override;
  1. 将以下内容添加到类构造函数中:
auto MeshAsset = ConstructorHelpers::FObjectFinder<UStaticMesh>(TEXT("StaticMesh'/Engine/BasicShapes/Cone.Cone'"));
if (MeshAsset.Object != nullptr)
{
  GetStaticMeshComponent()->SetStaticMesh(MeshAsset.Object);
  GetStaticMeshComponent()->bGenerateOverlapEvents = true;
}
GetStaticMeshComponent()->SetMobility(EComponentMobility::Movable);
ShowStaticMesh = true;
  1. 实现PostEditChangeProperty
void APostEditChangePropertyActor::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent)
{
  if (PropertyChangedEvent.Property != nullptr)
  {
    const FName PropertyName(PropertyChangedEvent.Property->GetFName());
    if (PropertyName == GET_MEMBER_NAME_CHECKED(APostEditChangePropertyActor, ShowStaticMesh))
    {
      if (GetStaticMeshComponent() != nullptr)
      {
        GetStaticMeshComponent()->SetVisibility(ShowStaticMesh);
      }
    }
  }
  Super::PostEditChangeProperty(PropertyChangedEvent);
}
  1. 编译代码并启动编辑器。

  2. 将类的实例拖入游戏世界,并验证切换ShowStaticMesh的布尔值是否切换编辑器视口中网格的可见性。操作方法…操作方法…

它的工作原理是...

  1. 我们基于StaticMeshActor创建一个新的Actor,以便通过静态网格轻松访问可视化表示。

  2. 添加UPROPERTY以提供我们要更改的属性,以触发PostEditChangeProperty事件。

  3. PostEditChangeProperty是在Actor中定义的虚函数。

  4. 因此,我们在我们的类中重写该函数。

  5. 在我们的类构造函数中,我们像往常一样初始化我们的网格,并将我们的bool属性的默认状态设置为与其控制的组件的可见性相匹配。

  6. PostEditChangeProperty中,我们首先检查属性是否有效。

  7. 假设它是有效的,我们使用GetFName()检索属性的名称。

  8. FNames在引擎内部以唯一值的表格形式存储。

  9. 接下来,我们需要使用GET_MEMBER_NAME_CHECKED宏。该宏接受多个参数。

  10. 第一个是要检查的类的名称。

  11. 第二个参数是要检查类的属性。

  12. 宏将在编译时验证类是否包含指定名称的成员。

  13. 我们将宏返回的类成员名称与我们的属性包含的名称进行比较。

  14. 如果它们相同,那么我们验证我们的StaticMeshComponent是否正确初始化。

  15. 如果是,我们将其可见性设置为与我们的ShowStaticMesh布尔值的值相匹配。

实现本地代码构造脚本

在蓝图中,构造脚本是一个事件图,它在附加到对象上的任何属性发生更改时运行-无论是通过在编辑器视口中拖动还是通过在详细信息面板中直接输入。

构造脚本允许对象根据其新位置“重建”自身,例如,或者根据用户选择的选项更改其包含的组件。

在使用虚幻引擎进行 C++编码时,等效的概念是OnConstruction函数。

如何实现...

  1. 创建一个名为AOnConstructionActor的新Actor,基于StaticMeshActor

  2. 将以下UPROPERTY添加到类中:

UPROPERTY(EditAnywhere)
bool ShowStaticMesh;
  1. 添加以下函数定义:
virtual void OnConstruction(const FTransform& Transform) override;
  1. 将以下内容添加到类构造函数中:
auto MeshAsset = ConstructorHelpers::FObjectFinder<UStaticMesh>(TEXT("StaticMesh'/Engine/BasicShapes/Cone.Cone'"));
if (MeshAsset.Object != nullptr)
{
  GetStaticMeshComponent()->SetStaticMesh(MeshAsset.Object);
  GetStaticMeshComponent()->bGenerateOverlapEvents = true;
}
GetStaticMeshComponent()->SetMobility(EComponentMobility::Movable);
ShowStaticMesh = true;
  1. 实现OnConstruction
void AOnConstructionActor::OnConstruction(const FTransform& Transform)
{
  GetStaticMeshComponent()->SetVisibility(ShowStaticMesh);
}
  1. 编译代码并启动编辑器。

  2. 将类的实例拖动到游戏世界中,并验证切换ShowStaticMesh布尔值是否切换编辑器视口中网格的可见性。

  3. 如果 C++的 Actor 被移动,目前OnConstruction不会运行。

  4. 为了测试这个,将断点放在你的OnConstruction函数中,然后将你的 Actor 移动到关卡中。

提示

要设置断点,请将光标放在所需行上,然后在 Visual Studio 中按下F9

  1. 您会注意到该函数不会被调用,但是如果切换ShowStaticMesh布尔值,它会被调用,从而触发断点。

注意

为了了解原因,请查看AActor::PostEditMove

UBlueprint* Blueprint = Cast<UBlueprint>(GetClass()->ClassGeneratedBy);
if(Blueprint && (Blueprint->bRunConstructionScriptOnDrag || bFinished) && !FLevelUtils::IsMovingLevel() )
{
  FNavigationLockContext NavLock(GetWorld(), ENavigationLockReason::AllowUnregister);
  RerunConstructionScripts();
}

这里的顶行将当前对象的UClass转换为UBlueprint,并且只有在类是蓝图时才会运行构造脚本和OnConstruction

它的工作原理是...

  1. 我们基于StaticMeshActor创建一个新的 Actor,以便通过静态网格轻松访问可视化表示。

  2. 添加UPROPERTY以提供我们要更改的属性-以触发PostEditChangeProperty事件。

  3. OnConstruction是在 Actor 中定义的虚函数。

  4. 因此,我们在我们的类中重写该函数。

  5. 在我们的类构造函数中,我们像往常一样初始化我们的网格,并将我们的bool属性的默认状态设置为与其控制的组件的可见性相匹配。

  6. OnConstruction中,Actor 使用任何需要进行重建的属性来重建自身。

  7. 对于这个简单的示例,我们将网格的可见性设置为与我们的ShowStaticMesh属性的值相匹配。

  8. 这也可以扩展到根据ShowStaticMesh变量的值更改其他值。

  9. 您会注意到,与前一个示例中使用PostEditChangeProperty显式过滤特定属性更改不同。

  10. OnConstruction脚本会在对象上的每个属性发生更改时完整运行。

  11. 它无法测试刚刚编辑的属性,因此您需要谨慎地将计算密集型代码放在其中。

创建一个新的编辑器模块

以下示例都与编辑器模式特定代码和引擎模块进行交互。因此,根据惯例,创建一个仅在引擎以编辑器模式运行时加载的新模块,以便我们可以将所有仅限于编辑器的代码放在其中。

操作步骤如下:

  1. 在文本编辑器(如记事本或 Notepad++)中打开项目的.uproject文件。

  2. 将以下粗体部分添加到文件中:

{
  "FileVersion": 3,
  "EngineAssociation": "4.11",
  "Category": "",
  "Description": "",
  "Modules": [
    {
      "Name": "UE4Cookbook",
      "Type": "Runtime",
      "LoadingPhase": "Default",
      "AdditionalDependencies": [
        "Engine",
        "CoreUObject"
      ]
    },
 {
 "Name": "UE4CookbookEditor",
 "Type": "Editor",
 "LoadingPhase": "PostEngineInit",
 "AdditionalDependencies": [
 "Engine",
 "CoreUObject"
 ]
 }
  ]
}
  1. 注意第一个模块之后第二组花括号前的逗号。

  2. 在源文件夹中,使用与您在uproject文件中指定的名称相同的名称创建一个新文件夹(在本例中为"UE4CookbookEditor")。

  3. 在这个新文件夹中,创建一个名为UE4CookbookEditor.Build.cs的文件。

  4. 将以下内容插入文件中:

using UnrealBuildTool;

public class UE4CookbookEditor : ModuleRules
{
  public UE4CookbookEditor(TargetInfo Target)
  {
    PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "RHI", "RenderCore", "ShaderCore" });
    PublicDependencyModuleNames.Add("UE4Cookbook");
    PrivateDependencyModuleNames.AddRange(new string[] { "UnrealEd" });
  }
}
  1. 创建一个名为UE4CookbookEditor.h的新文件,并添加以下内容:
#pragma once
#include "Engine.h"
#include "ModuleManager.h"
#include "UnrealEd.h"

class FUE4CookbookEditorModule: public IModuleInterface
{
};
  1. 最后,创建一个名为UE4CookbookEditor.cpp的新源文件。

  2. 添加以下代码:

#include "UE4CookbookEditor.h"
IMPLEMENT_GAME_MODULE(FUE4CookbookEditorModule, UE4CookbookEditor)
  1. 最后,如果您已经打开了 Visual Studio,请关闭它,然后右键单击.uproject文件,选择生成 Visual Studio 项目文件

  2. 您应该看到一个小窗口启动,显示进度条,然后关闭。操作步骤如下

  3. 现在可以启动 Visual Studio,验证 IDE 中是否可见新模块,并成功编译项目。

  4. 该模块现在已准备好进行下一组操作。

注意

在此编辑器模块中进行的代码更改不支持与运行时模块中的代码相同的热重载。如果出现提到更改生成的头文件的编译错误,请关闭编辑器,然后从 IDE 内部重新构建。

工作原理如下:

  1. Unreal 项目使用.uproject文件格式来指定有关项目的许多不同信息。

  2. 此信息用于通知头文件和构建工具关于组成此项目的模块,并用于代码生成和makefile创建。

  3. 该文件使用 JSON 样式的格式。

  4. 这些包括以下内容:

  • 项目应该在其中打开的引擎版本

  • 项目中使用的模块列表

  • 模块声明列表

  1. 每个模块声明都包含以下内容:
  • 模块的名称。

  • 模块的类型-它是一个编辑器模块(仅在编辑器构建中运行,可以访问仅限于编辑器的类)还是运行时模块(在编辑器和发布构建中运行)。

  • 模块的加载阶段-模块可以在程序启动的不同阶段加载。这个值指定了模块应该在哪个点加载,例如,如果有其他模块的依赖应该先加载。

  • 模块的依赖列表。这些是包含模块所依赖的导出函数或类的基本模块。

  1. 我们向uproject文件添加了一个新模块。该模块的名称是UE4CookbookEditor(按照惯例,对于编辑器模块,应该在主游戏模块后附加Editor)。

  2. 该模块被标记为编辑器模块,并设置为在基线引擎之后加载,以便可以使用在引擎代码中声明的类。

  3. 我们的模块的依赖关系暂时保持默认值。

  4. uproject文件修改为包含我们的新模块后,我们需要一个构建脚本。

  5. 构建脚本是用 C#编写的,名称为<ModuleName>.Build.cs

  6. 与 C++不同,C#不使用单独的头文件和实现文件-所有内容都在一个.cs文件中。

  7. 我们想要访问在UnrealBuildTool模块中声明的类,因此我们包含一个using语句来指示我们要访问该命名空间。

  8. 我们创建一个与我们的模块同名的public类,并继承自ModuleRules

  9. 在我们的构造函数中,我们将多个模块添加到此模块的依赖项中。

  10. 有私有依赖和公共依赖。根据ModuleRules类的代码,公共依赖是您模块的公共头文件依赖的模块。私有依赖是私有代码依赖的模块。在公共头文件和私有代码中都使用的内容应放入PublicDependencyModuleNames数组中。

  11. 请注意,我们的PublicDependencyModuleNames数组包含我们的主游戏模块。这是因为本章中的一些示例将扩展编辑器以更好地支持我们主游戏模块中定义的类。

  12. 现在,我们已经告诉构建系统通过项目文件构建新模块,并且已经指定了如何使用构建脚本构建模块,我们需要创建实际模块的 C++类。

  13. 我们创建一个包含引擎头文件、ModuleManager头文件和UnrealEd头文件的头文件。

  14. 我们包括ModuleManager,因为它定义了IModuleInterface,我们的模块将继承自该类。

  15. 我们还包括UnrealEd,因为我们正在编写一个需要访问编辑器功能的编辑器模块。

  16. 我们声明的类继承自IModuleInterface,并从通常的前缀F开始命名。

  17. .cpp文件中,我们包含了模块的头文件,然后使用IMPLEMENT_GAME_MODULE宏。

  18. IMPLEMENT_GAME_MODULE声明了一个导出的 C 函数InitializeModule(),它返回我们新模块类的实例。

  19. 这意味着 Unreal 可以简单地调用任何导出它的库上的InitializeModule()来检索对实际模块实现的引用,而不需要知道它是什么类。

  20. 添加了新模块后,我们现在需要重新构建 Visual Studio 解决方案,因此关闭 Visual Studio,然后使用上下文菜单重新生成项目文件。

  21. 重新构建项目后,新模块将在 Visual Studio 中可见,我们可以像往常一样向其添加代码。

创建新的工具栏按钮

如果您已经为编辑器创建了自定义工具或窗口,那么您可能需要一种让用户显示它的方法。最简单的方法是创建一个工具栏自定义,添加一个新的工具栏按钮,并在点击时显示您的窗口。

按照前面的示例创建一个新的引擎模块,因为我们需要它来初始化我们的工具栏自定义。

如何操作…

  1. 创建一个新的头文件,并插入以下类声明:
#pragma once
#include "Commands.h"
#include "EditorStyleSet.h"
/**
 * 
 */
class FCookbookCommands : public TCommands<FCookbookCommands>
{
  public:
  FCookbookCommands()
  :TCommands<FCookbookCommands>(FName(TEXT("UE4_Cookbook")), FText::FromString("Cookbook Commands"), NAME_None, FEditorStyle::GetStyleSetName()) 
  {
  };
  virtual void RegisterCommands() override;

  TSharedPtr<FUICommandInfo> MyButton;
};
  1. 通过在.cpp文件中放置以下内容来实现新类:
#include "UE4CookbookEditor.h"
#include "Commands.h"
#include "CookbookCommands.h"

void FCookbookCommands::RegisterCommands()
{
  #define LOCTEXT_NAMESPACE ""
  UI_COMMAND(MyButton, "Cookbook", "Demo Cookbook Toolbar Command", EUserInterfaceActionType::Button, FInputGesture());
  #undef LOCTEXT_NAMESPACE
}
  1. 在您的模块类中添加以下内容:
virtual void StartupModule() override;
virtual void ShutdownModule() override;
TSharedPtr<FExtender> ToolbarExtender;
TSharedPtr<const FExtensionBase> Extension;
void MyButton_Clicked()
{
  TSharedRef<SWindow> CookbookWindow = SNew(SWindow)
  .Title(FText::FromString(TEXT("Cookbook Window")))
  .ClientSize(FVector2D(800, 400))
  .SupportsMaximize(false)
  .SupportsMinimize(false);

  IMainFrameModule& MainFrameModule = FModuleManager::LoadModuleChecked<IMainFrameModule>(TEXT("MainFrame"));

  if (MainFrameModule.GetParentWindow().IsValid())
  {
    FSlateApplication::Get().AddWindowAsNativeChild(CookbookWindow,MainFrameModule.GetParentWindow().ToSharedRef());
  }
  else
  {
    FSlateApplication::Get().AddWindow(CookbookWindow);
  }
};
void AddToolbarExtension(FToolBarBuilder &builder)
{
  FSlateIcon IconBrush = FSlateIcon(FEditorStyle::GetStyleSetName(), "LevelEditor.ViewOptions", "LevelEditor.ViewOptions.Small");

  builder.AddToolBarButton(FCookbookCommands::Get().MyButton, NAME_None, FText::FromString("My Button"), FText::FromString("Click me to display a message"), IconBrush, NAME_None);
};
  1. 确保还#include您的命令类的头文件。

  2. 现在我们需要实现StartupModuleShutdownModule

void FUE4CookbookEditorModule::StartupModule()
{
  FCookbookCommands::Register();
  TSharedPtr<FUICommandList> CommandList = MakeShareable(new FUICommandList());
  CommandList->MapAction(FCookbookCommands::Get().MyButton, FExecuteAction::CreateRaw(this, &FUE4CookbookEditorModule::MyButton_Clicked), FCanExecuteAction());
  ToolbarExtender = MakeShareable(new FExtender());
  Extension = ToolbarExtender->AddToolBarExtension("Compile", EExtensionHook::Before, CommandList, FToolBarExtensionDelegate::CreateRaw(this, &FUE4CookbookEditorModule::AddToolbarExtension));

  FLevelEditorModule& LevelEditorModule = FModuleManager::LoadModuleChecked<FLevelEditorModule>("LevelEditor");
  LevelEditorModule.GetToolBarExtensibilityManager()->AddExtender(ToolbarExtender);
}

void FUE4CookbookEditorModule::ShutdownModule()
{
  ToolbarExtender->RemoveExtension(Extension.ToSharedRef());
  Extension.Reset();
  ToolbarExtender.Reset();
}
  1. 添加以下包含:
#include "LevelEditor.h"
#include "SlateBasics.h"
#include "MultiBoxExtender.h"
#include "Chapter8/CookbookCommands.h"
  1. 编译您的项目,并启动编辑器。

  2. 验证在主级别编辑器的工具栏上有一个新按钮,可以单击它打开一个新窗口:如何操作…

它是如何工作的…

  1. Unreal 的编辑器 UI 基于命令的概念。命令是一种设计模式,允许 UI 和它需要执行的操作之间的耦合度较低。

  2. 为了创建一个包含一组命令的类,需要继承自TCommands

  3. TCommands是一个模板类,利用了奇异递归模板模式CRTP)。CRTP 在Slate UI 代码中常用作创建编译时多态的一种方式。

  4. FCookbookCommands构造函数的初始化列表中,我们调用父类构造函数,传入多个参数。

  5. 第一个参数是命令集的名称,是一个简单的FName

  6. 第二个参数是一个工具提示/可读字符串,因此使用FText,以便在需要时支持本地化。

  7. 如果有一个命令的父组,第三个参数包含组的名称。否则,它包含NAME_None

  8. 构造函数的最后一个参数是包含命令集将使用的任何命令图标的 Slate 样式集。

  9. RegisterCommands()函数允许TCommands派生类创建它们所需的任何命令对象。从该函数返回的FUICommandInfo实例存储在Commands类中作为成员,以便可以将 UI 元素或函数绑定到命令上。

  10. 这就是为什么我们有一个成员变量TSharedPtr<FUICommandInfo> MyButton

  11. 在类的实现中,我们只需要在RegisterCommands中创建我们的命令。

  12. UI_COMMAND宏用于创建FUICommandInfo的实例,即使只是一个空的默认命名空间,也需要定义一个本地化命名空间。

  13. 因此,即使我们不打算使用本地化,我们仍需要用#defines来封装我们的UI_COMMAND调用,以设置LOCTEXT_NAMESPACE的有效值。

  14. 实际的UI_COMMAND宏接受多个参数。

  15. 第一个参数是用来存储FUICommandInfo的变量。

  16. 第二个参数是一个可读的命令名称。

  17. 第三个参数是命令的描述。

  18. 第四个参数是EUserInterfaceActionType。这个枚举实际上指定了正在创建的按钮的类型。它支持ButtonToggleButtonRadioButtonCheck作为有效类型。

  19. 按钮是简单的通用按钮。切换按钮存储开和关的状态。单选按钮类似于切换按钮,但与其他单选按钮分组,并且一次只能启用一个。最后,复选框在按钮旁边显示一个只读复选框。

  20. UI_COMMAND的最后一个参数是输入键组合,或者激活命令所需的键的组合。

  21. 这个参数主要用于为与该命令相关联的热键定义键组合,而不是按钮。因此,我们使用一个空的InputGesture

  22. 所以现在我们有了一组命令,但是我们还没有告诉引擎我们想要将这组命令添加到工具栏上显示的命令中。我们也还没有设置当按钮被点击时实际发生的事情。为了做到这一点,我们需要在模块开始时执行一些初始化操作,所以我们将一些代码放入StartupModule/ShutdownModule函数中。

  23. StartupModule中,我们调用之前定义的命令类上的静态Register函数。

  24. 然后,我们使用MakeShareable函数创建一个命令列表的共享指针。

  25. 在命令列表中,我们使用MapAction来创建一个映射或关联,将我们设置为FCookbookCommands的成员的UICommandInfo对象与我们希望在调用命令时执行的实际函数关联起来。

  26. 你会注意到,我们在这里没有明确设置任何关于如何调用命令的内容。

  27. 为了执行这个映射,我们调用MapAction函数。MapAction的第一个参数是一个FUICommandInfo对象,我们可以通过使用它的静态Get()方法从FCookbookCommands中检索实例来获取它。

  28. FCookbookCommands被实现为一个单例类,即一个在整个应用程序中存在的单个实例。你会在大多数地方看到这种模式——引擎中有一个可用的静态Get()方法。

  29. MapAction函数的第二个参数是一个绑定到在执行命令时要调用的函数的委托。

  30. 因为UE4CookbookEditorModule是一个原始的 C++类,而不是一个UObject,我们想要调用一个成员函数而不是一个static函数,所以我们使用CreateRaw来创建一个绑定到原始 C++成员函数的新委托。

  31. CreateRaw期望一个指向对象实例的指针,并且一个对该指针上要调用的函数的函数引用。

  32. MapAction的第三个参数是一个委托,用于调用以测试是否可以执行该操作。因为我们希望该命令始终可执行,所以我们可以使用一个简单的预定义委托,它始终返回true

  33. 通过将我们的命令与它应该调用的操作关联起来,我们现在需要告诉扩展系统我们想要向工具栏添加新的命令。

  34. 我们可以通过FExtender类来实现这一点,该类可用于扩展菜单、上下文菜单或工具栏。

  35. 我们最初创建了一个FExtender的实例作为共享指针,以便在模块关闭时我们的扩展未初始化。

  36. 然后,我们在我们的新扩展器上调用AddToolBarExtension,将结果存储在共享指针中,以便在模块未初始化时将其移除。

  37. AddToolBarExtension的第一个参数是我们要添加扩展的扩展点的名称。

  38. 要找到我们要放置扩展的位置,我们首先需要在编辑器 UI 中打开扩展点的显示。

  39. 为此,请在编辑器的Edit菜单中打开Editor Preferences它是如何工作的...

  40. 打开General | Miscellaneous,然后选择Display UIExtension Points它是如何工作的...

  41. 重新启动编辑器,您应该会看到覆盖在编辑器 UI 上的绿色文本,如下面的屏幕截图所示:它是如何工作的...

  42. 绿色文本表示UIExtensionPoint,文本的值是我们应该提供给AddToolBarExtension函数的字符串。

  43. 在本示例中,我们将在Compile扩展点中添加我们的扩展,但是当然,您可以使用任何其他您希望的扩展点。

  44. 重要的是要注意,将工具栏扩展添加到菜单扩展点将会静默失败,反之亦然。

  45. AddToolBarExtension的第二个参数是相对于指定的扩展点的位置锚点。我们选择了FExtensionHook::Before,所以我们的图标将显示在编译点之前。

  46. 下一个参数是包含映射操作的命令列表。

  47. 最后,最后一个参数是一个委托,负责将 UI 控件实际添加到我们之前指定的扩展点和锚点的工具栏上。

  48. 委托绑定到一个具有形式 void(*func)(FToolBarBuilderbuilder)的函数。在这个实例中,它是我们模块类中定义的一个名为AddToolbarExtension的函数。

  49. 当调用该函数时,调用在作为函数参数传入的FToolBarBuilder实例上的命令,添加 UI 元素将将这些元素应用到我们指定的 UI 位置。

  50. 最后,我们需要在此函数中加载级别编辑器模块,以便我们可以将我们的扩展器添加到级别编辑器中的主工具栏中。

  51. 通常情况下,我们可以使用ModuleManager加载一个模块并返回对它的引用。

  52. 有了这个引用,我们可以获取模块的工具栏扩展性管理器,并告诉它添加我们的扩展器。

  53. 虽然一开始可能会感到繁琐,但这样做的目的是允许您将相同的工具栏扩展应用于不同模块中的多个工具栏,以便在不同的编辑器窗口之间创建一致的 UI 布局。

  54. 当然,初始化我们的扩展的对应操作是在模块卸载时将其移除。为此,我们从扩展器中移除我们的扩展,然后将 Extender 和扩展的共享指针置空,以回收它们的内存分配。

  55. AddToolBarExtension函数在编辑器模块中负责实际将 UI 元素添加到工具栏中,这些 UI 元素可以调用我们的命令。

  56. 它通过在传入的FToolBarBuilder实例上调用函数来实现这一点。

  57. 首先,我们使用FSlateIcon构造函数为我们的新工具栏按钮获取适当的图标。

  58. 有了加载的图标,我们在builder实例上调用AddToolBarButton

  59. AddToolbarButton有许多参数。

  60. 第一个参数是要绑定的命令 - 您会注意到它与我们之前绑定操作到命令时访问的MyButton成员相同。

  61. 第二个参数是我们之前指定的扩展挂钩的覆盖,但我们不想覆盖它,所以我们可以使用NAME_None

  62. 第三个参数是我们创建的新按钮的标签覆盖。

  63. 第四个参数是新按钮的工具提示。

  64. 倒数第二个参数是按钮的图标,最后一个参数是用于引用此按钮元素以支持突出显示的名称,如果您希望使用编辑器内教程框架。

创建新菜单项

创建新菜单项的工作流程与创建新工具栏按钮的工作流程几乎相同,因此本教程将在前一个教程的基础上进行构建,并向您展示如何将其中创建的命令添加到菜单而不是工具栏。

操作步骤...

  1. 在您的module类中创建一个新函数:
void AddMenuExtension(FMenuBuilder &builder)
{
  FSlateIcon IconBrush = FSlateIcon(FEditorStyle::GetStyleSetName(), "LevelEditor.ViewOptions", "LevelEditor.ViewOptions.Small");

  builder.AddMenuEntry(FCookbookCommands::Get().MyButton);
};
  1. StartupModule函数中找到以下代码:
Extension = ToolbarExtender->AddToolBarExtension("Compile", EExtensionHook::Before, CommandList, FToolBarExtensionDelegate::CreateRaw(this, &FUE4CookbookEditorModule::AddToolbarExtension));
LevelEditorModule.GetToolBarExtensibilityManager()->AddExtender(ToolbarExtender);
  1. 用以下代码替换前面的代码:
Extension = ToolbarExtender->AddMenuExtension("LevelEditor", EExtensionHook::Before, CommandList, FMenuExtensionDelegate::CreateRaw(this, &FUE4CookbookEditorModule::AddMenuExtension));
LevelEditorModule.GetMenuExtensibilityManager()->AddExtender(ToolbarExtender);
  1. 编译代码并启动编辑器。

  2. 验证现在在Window菜单下是否有一个菜单项,当单击时显示Cookbook窗口。如果您按照前面的教程操作,您还将看到列出 UI 扩展点的绿色文本,包括我们在此教程中使用的扩展点(LevelEditor)。操作步骤...

工作原理如下...

  1. 您会注意到ToolbarExtender的类型是FExtender而不是FToolbarExtenderFMenuExtender

  2. 通过使用通用的FExtender类而不是特定的子类,框架允许您创建一系列可以用于菜单或工具栏的命令-函数映射。实际添加 UI 控件的委托(在本例中为AddMenuExtension)可以将这些控件链接到您的FExtender中的一部分命令。

  3. 这样,您就不需要为不同类型的扩展创建不同的TCommands类,并且可以将命令放入单个中央类中,而不管这些命令从 UI 的哪个位置调用。

  4. 因此,唯一需要的更改如下:

  5. AddToolBarExtension的调用与AddMenuExtension交换。

  6. 创建一个可以绑定到FMenuExtensionDelegate而不是FToolbarExtensionDelegate的函数。

  7. 将扩展器添加到菜单扩展性管理器而不是工具栏扩展性管理器。

创建一个新的编辑器窗口

自定义编辑器窗口在您具有具有用户可配置设置的新工具或希望向使用您的自定义编辑器的人显示一些信息时非常有用。

在开始之前,请确保按照本章前面的教程创建了一个编辑器模块。

阅读创建新菜单项创建新工具栏按钮的任一教程,以便您可以在编辑器中创建一个按钮,该按钮将启动我们的新窗口。

操作步骤...

  1. 在命令的绑定函数中,添加以下代码:
TSharedRef<SWindow> CookbookWindow = SNew(SWindow)
.Title(FText::FromString(TEXT("Cookbook Window")))
.ClientSize(FVector2D(800, 400))
.SupportsMaximize(false)
.SupportsMinimize(false)
[
  SNew(SVerticalBox)
  +SVerticalBox::Slot()
  .HAlign(HAlign_Center)
  .VAlign(VAlign_Center)
  [
    SNew(STextBlock)
    .Text(FText::FromString(TEXT("Hello from Slate")))
  ]
];
IMainFrameModule& MainFrameModule = FModuleManager::LoadModuleChecked<IMainFrameModule>(TEXT("MainFrame"));

if (MainFrameModule.GetParentWindow().IsValid())
{
  FSlateApplication::Get().AddWindowAsNativeChild(CookbookWindow, MainFrameModule.GetParentWindow().ToSharedRef());
}
else
{
  FSlateApplication::Get().AddWindow(CookbookWindow);
}
  1. 编译代码并启动编辑器。

  2. 当您激活您创建的命令时,无论是选择自定义菜单选项还是您添加的工具栏选项,您都应该看到窗口已显示在中间的一些居中文本中:操作步骤...

工作原理如下...

  1. 如自解释,您的新编辑器窗口不会自行显示,因此,在本教程开始时提到,您应该实现一个自定义菜单或工具栏按钮或控制台命令,我们可以使用它来触发显示我们的新窗口。

  2. 所有 Slate 的小部件通常以TSharedRef<>TSharedPtr<>的形式进行交互。

  3. SNew()函数返回一个以请求的小部件类为模板的TSharedRef

  4. 正如本章其他地方提到的,Slate 小部件有许多它们实现的函数,这些函数都返回调用该函数的对象。这允许在创建时使用方法链来配置对象。

  5. 这就是允许使用 Slate 语法<Widget>.Property(Value).Property(Value)的原因。

  6. 在这个示例中,我们设置的小部件属性包括窗口标题、窗口大小以及窗口是否可以最大化和最小化。

  7. 一旦小部件上的所有必需属性都被设置好,括号运算符([])可以用来指定要放置在小部件内部的内容,例如,在按钮内部放置图片或标签。

  8. SWindow是一个顶级小部件,只有一个用于子小部件的 slot,所以我们不需要为它自己添加一个 slot。我们通过在括号内创建内容来将内容放入该 slot 中。

  9. 我们创建的内容是SVerticalBox,它是一个可以有任意数量的子小部件的小部件,这些子小部件以垂直列表的形式显示。

  10. 对于我们想要放置在垂直列表中的每个小部件,我们需要创建一个slot

  11. 做到这一点最简单的方法是使用重载的+运算符和SVerticalBox::Slot()函数。

  12. Slot()返回一个像其他小部件一样的小部件,所以我们可以像在SWindow上设置属性一样在其上设置属性。

  13. 这个示例使用HAlignVAlign来使 Slot 的内容在水平和垂直轴上居中。

  14. Slot有一个单独的子小部件,并且它是在[]运算符中创建的,就像对于SWindow一样。

  15. Slot内容中,我们创建了一个带有一些自定义文本的文本块。

  16. 我们的新SWindow现在已经添加了其子小部件,但还没有显示出来,因为它还没有添加到窗口层级中。

  17. 主框架模块用于检查是否有顶级编辑器窗口,如果存在,则将我们的新窗口添加为子窗口。

  18. 如果没有顶级窗口要作为子窗口添加,那么我们可以使用 Slate 应用程序单例将我们的窗口添加到没有父窗口的情况下。

  19. 如果你想要查看我们创建的窗口的层次结构,你可以使用 Slate 小部件反射器,它可以通过窗口 | 开发者工具 | 小部件反射器访问。

  20. 如果你选择选择实时小部件,并将光标悬停在我们自定义窗口中央的文本上,你将能够看到包含我们自定义小部件的SWindow的层次结构。它的工作原理…

另请参阅

  • 第九章,“用户界面 - UI 和 UMG”,讲解了 UI,并且会向你展示如何向你的新自定义窗口添加额外的元素。

创建一个新的资产类型

在项目的某个时候,你可能需要创建一个新的自定义资产类,例如,用于在 RPG 中存储对话数据的资产。

为了正确地将它们与内容浏览器集成,你需要创建一个新的资产类型。

如何操作…

  1. 创建一个基于UObject的自定义资产:
#pragma once

#include "Object.h"
#include "MyCustomAsset.generated.h"

/**
 * 
 */
UCLASS()
class UE4COOKBOOK_API UMyCustomAsset : public UObject
{
  GENERATED_BODY()
  public:
  UPROPERTY(EditAnywhere, Category = "Custom Asset")
  FString Name;
};
  1. 创建一个名为UCustomAssetFactory的类,基于UFactory,并重写FactoryCreateNew方法:
#pragma once

#include "Factories/Factory.h"
#include "CustomAssetFactory.generated.h"

/**
 * 
 */
UCLASS()
class UE4COOKBOOK_API UCustomAssetFactory : public UFactory
{
  GENERATED_BODY()

  public:
  UCustomAssetFactory();

  virtual UObject* FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn, FName CallingContext) override;
};
  1. 实现这个类:
#include "UE4Cookbook.h"
#include "MyCustomAsset.h"
#include "CustomAssetFactory.h"

UCustomAssetFactory::UCustomAssetFactory()
:Super()
{
  bCreateNew = true;
  bEditAfterNew = true;
  SupportedClass = UMyCustomAsset::StaticClass();
}

UObject* UCustomAssetFactory::FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn, FName CallingContext)
{
  auto NewObjectAsset = NewObject<UMyCustomAsset>(InParent, InClass, InName, Flags);
  return NewObjectAsset;
}
  1. 编译你的代码,并打开编辑器。

  2. 内容浏览器中右键单击,在创建高级资产部分的杂项选项卡下,你应该能够看到你的新类,并能够创建你的新自定义类型的实例。如何操作…

它的工作原理是…

  1. 第一个类是实际在游戏运行时存在的对象。它可以是纹理、数据文件或曲线数据,根据你的需求而定。

  2. 对于这个示例,最简单的例子是一个具有FString属性来存储名称的资产。

  3. 该属性被标记为UPROPERTY,以便它保留在内存中,并且额外标记为EditAnywhere,以便在默认对象和其实例上都可以编辑它。

  4. 第二个类是Factory。虚幻使用Factory设计模式来创建资产的实例。

  5. 这意味着有一个通用的基础Factory,它使用虚拟方法来声明对象创建的接口,然后Factory的子类负责创建实际的对象。

  6. 这种方法的优点是,用户创建的子类可以在需要时实例化其自己的子类;它将决定创建哪个对象的实现细节隐藏在请求创建的对象之外。

  7. UFactory作为基类,我们包含适当的头文件。

  8. 构造函数被重写,因为在默认构造函数运行后,我们希望为新工厂设置一些属性。

  9. bCreateNew表示工厂当前能够从头开始创建对象的新实例。

  10. bEditAfterNew表示我们希望在创建后立即编辑新创建的对象。

  11. SupportedClass变量是一个包含有关工厂将创建的对象类型的反射信息的UClass的实例。

  12. 我们的UFactory子类最重要的功能是实际的工厂方法——FactoryCreateNew

  13. FactoryCreateNew负责确定应该创建的对象类型,并使用NewObject构造该类型的实例。它将以下参数传递给NewObject调用。

  14. InClass是将要构造的对象的类。

  15. InParent是应该包含将要创建的新对象的对象。如果未指定此参数,则假定对象将进入临时包,这意味着它不会自动保存。

  16. Name是要创建的对象的名称。

  17. Flags是一个位掩码,用于控制创建标志,例如使对象在其所包含的包之外可见。

  18. FactoryCreateNew中,可以根据需要决定实例化哪个子类。还可以执行其他初始化操作;例如,如果有需要手动实例化或初始化的子对象,可以在此处添加。

  19. 此函数的引擎代码示例如下:

UObject* UCameraAnimFactory::FactoryCreateNew(UClass* Class,UObject* InParent,FName Name,EObjectFlags Flags,UObject* Context,FFeedbackContext* Warn)
{
  UCameraAnim* NewCamAnim = NewObject<UCameraAnim>(InParent, Class, Name, Flags);
  NewCamAnim->CameraInterpGroup = NewObject<UInterpGroupCamera>(NewCamAnim);
  NewCamAnim->CameraInterpGroup->GroupName = Name;
  return NewCamAnim;
}
  1. 如此所示,这里有第二次调用NewObject来填充NewCamAnim实例的CameraInterpGroup成员。

另请参阅

  • 本章前面的“编辑类属性在编辑器中的不同位置”配方为EditAnywhere属性指定器提供了更多上下文。

为资产创建自定义上下文菜单项

自定义资产类型通常具有您希望能够对其执行的特殊功能。例如,将图像转换为精灵是您不希望添加到任何其他资产类型的选项。您可以为特定的资产类型创建自定义上下文菜单项,以使这些功能对用户可用。

操作步骤

  1. 创建一个基于FAssetTypeActions_Base的新类。您需要在头文件中包含AssetTypeActions_Base.h

  2. 在类中重写以下虚拟函数:

virtual bool HasActions(const TArray<UObject*>& InObjects) const override;
virtual void GetActions(const TArray<UObject*>& InObjects, FMenuBuilder& MenuBuilder) override;
virtual FText GetName() const override;
virtual UClass* GetSupportedClass() const override;

virtual FColor GetTypeColor() const override;
virtual uint32 GetCategories() override;
  1. 声明以下函数:
void MyCustomAssetContext_Clicked();
  1. .cpp文件中实现声明的函数:
bool FMyCustomAssetActions::HasActions(const TArray<UObject*>& InObjects) const
{
  return true;
}

void FMyCustomAssetActions::GetActions(const TArray<UObject*>& InObjects, FMenuBuilder& MenuBuilder)
{
  MenuBuilder.AddMenuEntry(
  FText::FromString("CustomAssetAction"),
  FText::FromString("Action from Cookbook Recipe"),
  FSlateIcon(FEditorStyle::GetStyleSetName(), "LevelEditor.ViewOptions"),
  FUIAction(
  FExecuteAction::CreateRaw(this, &FMyCustomAssetActions::MyCustomAssetContext_Clicked),
  FCanExecuteAction()));
}

uint32 FMyCustomAssetActions::GetCategories()
{
  return EAssetTypeCategories::Misc;
}
FText FMyCustomAssetActions::GetName() const
{
  return FText::FromString(TEXT("My Custom Asset"));
}
UClass* FMyCustomAssetActions::GetSupportedClass() const
{
  return UMyCustomAsset::StaticClass();
}

FColor FMyCustomAssetActions::GetTypeColor() const
{
  return FColor::Emerald;
}
voidFMyCustomAssetActions::MyCustomAssetContext_Clicked()
{
  TSharedRef<SWindow> CookbookWindow = SNew(SWindow)
  .Title(FText::FromString(TEXT("Cookbook Window")))
  .ClientSize(FVector2D(800, 400))
  .SupportsMaximize(false)
  .SupportsMinimize(false);

  IMainFrameModule& MainFrameModule = FModuleManager::LoadModuleChecked<IMainFrameModule>(TEXT("MainFrame"));

  if (MainFrameModule.GetParentWindow().IsValid())
  {
    FSlateApplication::Get().AddWindowAsNativeChild(CookbookWindow, MainFrameModule.GetParentWindow().ToSharedRef());
  }
  else
  {
    FSlateApplication::Get().AddWindow(CookbookWindow);
  }
};
  1. 在编辑器模块中,将以下代码添加到StartupModule()函数中:
IAssetTools& AssetTools = FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools").Get();

auto Actions =MakeShareable(new FMyCustomAssetActions);
AssetTools.RegisterAssetTypeActions(Actions);
CreatedAssetTypeActions.Add(Actions);
  1. 在模块的ShutdownModule()函数中添加以下内容:
IAssetTools& AssetTools = FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools").Get();

for (auto Action : CreatedAssetTypeActions)
{
  AssetTools.UnregisterAssetTypeActions(Action.ToSharedRef());
}
  1. 编译项目并启动编辑器。

  2. 内容浏览器中创建自定义资产的实例。

  3. 右键单击您的新资产,查看上下文菜单中的自定义命令。操作步骤

  4. 选择CustomAssetAction命令以显示一个新的空白编辑器窗口。

工作原理

  1. 所有特定于资产类型的上下文菜单命令的基类是FAssetTypeActions_Base,因此我们需要从该类继承。

  2. FAssetTypeActions_Base是一个抽象类,定义了一些虚拟函数,允许扩展上下文菜单。包含这些虚拟函数的原始信息的接口可以在IAssetTypeActions.h中找到。

  3. 我们还声明了一个函数,将其绑定到我们自定义的上下文菜单项。

  4. IAssetTypeActions::HasActions(const TArray<UObject*>& InObjects)是引擎代码调用的函数,用于查看我们的AssetTypeActions类是否包含可以应用于所选对象的任何操作。

  5. 如果HasActions函数返回true,则调用IAssetTypeActions::GetActions(const TArray<UObject*>& InObjects, class FMenuBuilder& MenuBuilder)。它调用MenuBuilder上的函数来为我们提供的操作创建菜单选项。

  6. IAssetTypeActions::GetName()返回此类的名称。

  7. IAssetTypeActions::GetSupportedClass()返回我们的操作类支持的UClass的实例。

  8. IAssetTypeActions::GetTypeColor()返回与此类和操作相关联的颜色。

  9. IAssetTypeActions::GetCategories()返回适用于资产的类别。这用于更改在上下文菜单中显示的操作所属的类别。

  10. 我们重写的HasActions的实现只是在所有情况下返回true,依赖于基于GetSupportedClass结果的过滤。

  11. GetActions的实现中,我们可以在作为函数参数给出的MenuBuilder对象上调用一些函数。MenuBuilder是作为引用传递的,所以我们函数所做的任何更改在函数返回后仍然存在。

  12. AddMenuEntry有一些参数。第一个参数是操作本身的名称。这个名称将在上下文菜单中可见。名称是一个FText,所以如果需要,它可以进行本地化。为了简单起见,我们从字符串字面量构造FText,不关心多语言支持。

  13. 第二个参数也是FText,我们通过调用FText::FromString来构造它。如果用户在我们的命令上悬停的时间超过一小段时间,此参数是显示在工具提示中的文本。

  14. 下一个参数是命令的FSlateIcon,它是从编辑器样式集中的LevelEditor.ViewOptions图标构造的。

  15. 这个函数的最后一个参数是一个FUIAction实例。FUIAction是一个委托绑定的包装器,所以我们使用FExecuteAction::CreateRaw将命令绑定到FMyCustomAssetActions的这个实例上的MyCustomAsset_Clicked函数。

  16. 这意味着当菜单项被点击时,我们的MyCustomAssetContext_Clicked函数将被执行。

  17. 我们的GetName的实现返回我们资产类型的名称。如果我们没有自己设置缩略图,这个字符串将用于我们的资产的缩略图上,除了在我们的自定义资产所在的菜单部分的标题中使用。

  18. 正如你所期望的,GetSupportedClass的实现返回UMyCustomAsset::StaticClass(),因为这是我们希望我们的操作作用的资产类型。

  19. GetTypeColor()返回在内容浏览器中用于颜色编码的颜色,该颜色用于资产缩略图底部的条中。我在这里使用了 Emerald,但任何任意的颜色都可以工作。

  20. 这个配方的真正工作马是MyCustomAssetContext_Clicked()函数。

  21. 这个函数的第一件事是创建一个新的SWindow实例。

  22. SWindow是 Slate 窗口,是 Slate UI 框架中的一个类。

  23. Slate 小部件使用SNew函数创建,该函数返回所请求的小部件的实例。

  24. Slate 使用builder设计模式,这意味着在SNew返回正在操作的对象的引用之后,所有链接在其后的函数都返回对该对象的引用。

  25. 在这个函数中,我们创建了我们的新SWindow,然后设置窗口标题、其客户区大小或区域以及是否可以最大化或最小化。

  26. 准备好我们的新窗口后,我们需要获取对编辑器的根窗口的引用,以便将我们的窗口添加到层次结构中并显示出来。

  27. 我们使用IMainFrameModule类来实现这一点。它是一个模块,所以我们使用模块管理器来加载它。

  28. 如果无法加载模块,LoadModuleChecked将断言,因此我们不需要检查它。

  29. 如果模块已加载,我们检查是否有一个有效的父窗口。如果该窗口有效,则使用FSlateApplication::AddWindowAsNativeChild将我们的窗口作为顶级父窗口的子窗口添加。

  30. 如果我们没有顶级父窗口,该函数将使用AddWindow将新窗口添加到层次结构中的另一个窗口而不将其作为其子窗口。

  31. 现在我们有了一个类,它将在我们的自定义 Asset 类型上显示自定义操作,但我们实际上需要告诉引擎它应该要求我们的类处理该类型的自定义操作。为了做到这一点,我们需要使用 Asset Tools 模块注册我们的类。

  32. 最好的方法是在加载编辑器模块时注册我们的类,并在关闭时取消注册。

  33. 因此,我们将代码放入StartupModuleShutdownModule函数中。

  34. StartupModule中,我们使用Module Manager加载 Asset Tools 模块。

  35. 加载模块后,我们创建一个新的共享指针,引用我们自定义的 Asset actions 类的实例。

  36. 我们只需要调用AssetModule.RegisterAssetTypeActions,并传入我们的 actions 类的实例。

  37. 然后,我们需要存储对该Actions实例的引用,以便以后可以取消注册它。

  38. 此示例代码使用一个数组来存储所有创建的 asset actions,以便我们还可以为其他类添加自定义操作。

  39. ShutdownModule中,我们再次获取 Asset Tools 模块的实例。

  40. 使用基于范围的 for 循环,我们遍历之前填充的Actions实例数组,并调用UnregisterAssetTypeActions,传入我们的Actions类以进行取消注册。

  41. 注册了我们的类后,编辑器已被指示询问我们注册的类是否可以处理右键单击的资产。

  42. 如果资产是 Custom Asset 类的实例,则其StaticClass将与GetSupportedClass返回的类匹配。然后编辑器将调用GetActions并显示由我们对该函数的实现所做的更改的菜单。

  43. 当点击CustomAssetAction按钮时,我们通过创建的委托调用我们的自定义MyCustomAssetContext_Clicked函数。

创建新的控制台命令

在开发过程中,控制台命令可以非常有用,允许开发人员或测试人员轻松绕过内容或禁用与当前运行的测试不相关的机制。最常见的实现方式是通过控制台命令,在运行时调用函数。可以使用波浪线键(~)或键盘字母数字区域左上角的等效键来访问控制台。

创建新的控制台命令

准备工作

如果您还没有按照创建新的编辑器模块的步骤进行操作,请按照该步骤进行操作,因为此步骤需要一个地方来初始化和注册控制台命令。

操作步骤如下...

  1. 打开编辑器模块的头文件,并添加以下代码:
IConsoleCommand* DisplayTestCommand;
IConsoleCommand* DisplayUserSpecifiedWindow;
  1. StartupModule的实现中添加以下内容:
DisplayTestCommand = IConsoleManager::Get().RegisterConsoleCommand(TEXT("DisplayTestCommandWindow"), TEXT("test"), FConsoleCommandDelegate::CreateRaw(this, &FUE4CookbookEditorModule::DisplayWindow, FString(TEXT("Test Command Window"))), ECVF_Default);
DisplayUserSpecifiedWindow= IConsoleManager::Get().RegisterConsoleCommand(TEXT("DisplayWindow"), TEXT("test"), FConsoleCommandWithArgsDelegate::CreateLambda(
  &
  {
    FString WindowTitle;
    for (FString Arg : Args)
    {
      WindowTitle +=Arg;
      WindowTitle.AppendChar(' ');
    }
    this->DisplayWindow(WindowTitle);
  }
), ECVF_Default);
  1. ShutdownModule中,添加以下内容:
If (DisplayTestCommand)
{
  IConsoleManager::Get().UnregisterConsoleObject(DisplayTestCommand);
  DisplayTestCommand = nullptr;
}
If (DisplayUserSpecifiedWindow)
{
  IConsoleManager::Get().UnregisterConsoleObject(DisplayTestCommand);
  DisplayTestCommand = nullptr;
}
  1. 在编辑器模块中实现以下函数:
void DisplayWindow(FString WindowTitle)
{
  TSharedRef<SWindow> CookbookWindow = SNew(SWindow)
  .Title(FText::FromString(WindowTitle))
  .ClientSize(FVector2D(800, 400))
  .SupportsMaximize(false)
  .SupportsMinimize(false);
  IMainFrameModule& MainFrameModule = FModuleManager::LoadModuleChecked<IMainFrameModule>(TEXT("MainFrame"));
  if (MainFrameModule.GetParentWindow().IsValid())
  {
    FSlateApplication::Get().AddWindowAsNativeChild(CookbookWindow, MainFrameModule.GetParentWindow().ToSharedRef());
  }
  else
  {
    FSlateApplication::Get().AddWindow(CookbookWindow);
  }
}
  1. 编译代码并启动编辑器。

  2. 播放关卡,然后按下波浪线键打开控制台。

  3. 输入DisplayTestCommandWindow,然后按下Enter键。操作步骤如下...

  4. 您应该看到我们的教程窗口打开:操作步骤如下...

工作原理...

  1. 控制台命令通常由一个模块提供。在加载模块时,让模块创建命令的最佳方法是将代码放在StartupModule方法中。

  2. IConsoleManager是包含引擎控制台功能的模块。

  3. 由于它是核心模块的子模块,我们不需要在构建脚本中添加任何额外的信息来链接其他模块。

  4. 为了调用控制台管理器内的函数,我们需要获取对引擎正在使用的当前IConsoleManager实例的引用。为此,我们调用静态的Get函数,它返回一个对模块的引用,类似于单例模式。

  5. RegisterConsoleCommand是我们可以用来添加新的控制台命令并在控制台中使其可用的函数:

virtual IConsoleCommand* RegisterConsoleCommand(const TCHAR* Name, const TCHAR* Help, const FConsoleCommandDelegate& Command, uint32 Flags);
  1. 函数的参数如下:

  2. Name: 用户将要键入的实际控制台命令。它不应包含空格。

  3. Help: 当用户在控制台中查看命令时显示的工具提示。如果控制台命令带有参数,这是一个向用户显示用法信息的好地方。

  4. Command: 这是当用户输入命令时将执行的实际函数委托。

  5. Flags: 这些标志控制命令在发布版本中的可见性,并且也用于控制台变量。ECVF_Default指定默认行为,其中命令可见,并且在发布版本中没有可用性限制。

  6. 为了创建适当委托的实例,我们使用FConsoleCommand委托类型上的CreateRaw静态函数。这使我们可以将原始的 C++函数绑定到委托上。在函数引用之后提供的额外参数FString "Test Command Window"是一个在编译时定义的参数,传递给委托,以便最终用户不必指定窗口名称。

  7. 第二个控制台命令DisplayUserSpecifiedWindow演示了使用控制台命令参数的用法。

  8. 与此控制台命令的主要区别是,除了用户调用它的不同名称之外,还使用了FConsoleCommandWithArgsDelegate和其中的CreateLambda函数。

  9. 这个函数允许我们将一个匿名函数绑定到一个委托上。当你想要包装或适应一个函数,使其签名与特定委托的签名匹配时,它特别方便。

  10. 在我们特定的用例中,FConsoleCommandWithArgsDelegate的类型指定函数应该接受一个const TArray的 FStrings。我们的DisplayWindow函数接受一个单独的FString来指定窗口标题,所以我们需要以某种方式将控制台命令的所有参数连接成一个单独的FString来用作我们的窗口标题。

  11. lambda 函数允许我们在将FString传递给实际的DisplayWindow函数之前执行此操作。

  12. 函数的第一行&指定了这个 lambda 或匿名函数希望通过引用捕获声明函数的上下文,通过在捕获选项[&]中包含和号。

  13. 第二部分与普通函数声明相同,指定了我们的 lambda 以const TArray作为参数,其中包含名为Args的 FStrings。

  14. 在 lambda 主体中,我们创建了一个新的FString,并将组成我们参数的字符串连接在一起,它们之间添加一个空格来分隔它们,以便我们不会得到没有空格的标题。

  15. 为了简洁起见,它使用了基于范围的for循环来遍历它们并执行连接操作。

  16. 一旦它们都被连接起来,我们使用之前提到的&运算符捕获的this指针来调用DisplayWindow并传入我们的新标题。

  17. 为了在模块卸载时删除控制台命令,我们需要保持对控制台命令对象的引用。

  18. 为了实现这一点,我们在模块中创建了一个名为DisplayTestCommand的类型为IConsoleCommand*的成员变量。当我们执行RegisterConsoleCommand函数时,它返回一个指向控制台命令对象的指针,我们可以稍后用作句柄。

  19. 这使我们能够根据游戏玩法或其他因素在运行时启用或禁用控制台命令。

  20. ShutdownModule中,我们检查DisplayTestCommand是否引用有效的控制台命令对象。如果是,我们获取对IConsoleManager对象的引用,并调用UnregisterConsoleCommand,传入我们在调用RegisterConsoleCommand时存储的指针。

  21. 调用UnregisterConsoleCommand通过传入的指针删除IConsoleCommand实例,因此我们不需要自己deallocate内存,只需将DisplayTestCommand重置为nullptr,以确保旧指针不会悬空。

  22. DisplayWindow函数以FString参数形式接受窗口标题。这使我们可以使用带有参数的控制台命令来指定标题,或者使用有效负载参数来为其他命令硬编码标题。

  23. 该函数本身使用一个名为SNew()的函数来分配和创建一个SWindow对象。

  24. SWindow是一个 Slate 窗口,它是使用 Slate UI 框架的顶级窗口。

  25. Slate 使用Builder设计模式来方便配置新窗口。

  26. 这里使用的TitleClientSizeSupportsMaximizeSupportsMinimize函数都是SWindow的成员函数,并且它们返回一个SWindow的引用(通常是调用该方法的对象,但有时是使用新配置构造的新对象)。

  27. 所有这些成员方法返回配置对象的引用,使我们能够将这些方法调用链接在一起,以创建所需的配置对象。

  28. DisplayWindow中使用的函数创建了一个基于函数参数的新顶级窗口。它的宽度为 800x400 像素,不能最大化或最小化。

  29. 创建了我们的新窗口后,我们获取对主应用程序框架模块的引用。如果编辑器的顶级窗口存在且有效,我们将我们的新窗口实例作为该顶级窗口的子窗口添加。

  30. 为此,我们获取对 Slate 接口的引用,并调用AddWindowAsNativeChild将我们的窗口插入层次结构中。

  31. 如果没有有效的顶级窗口,我们不需要将我们的新窗口添加为任何窗口的子窗口,所以我们可以简单地调用AddWindow并传入我们的新窗口实例。

另请参阅

  • 有关委托的更多信息,请参阅第五章,处理事件和委托。它更详细地解释了有效负载变量。

  • 有关 Slate 的更多信息,请参阅第九章,用户界面

为蓝图创建新的图形引脚可视化器

在蓝图系统中,我们可以使用我们的MyCustomAsset类的实例作为变量,只要我们在其UCLASS宏中将该类标记为BlueprintType

然而,默认情况下,我们的新资产只是被视为UObject,我们无法访问其任何成员:

为蓝图创建新的图形引脚可视化器

对于某些类型的资产,我们可能希望以与FVector等类似的方式启用对字面值的内联编辑:

为蓝图创建新的图形引脚可视化器

为了实现这一点,我们需要使用一个Graph Pin可视化器。本教程将向您展示如何使用您定义的自定义小部件来启用任意类型的内联编辑。

如何操作...

  1. 创建一个名为MyCustomAssetPinFactory.h的新头文件。

  2. 在头文件中,添加以下代码:

#pragma once
#include "EdGraphUtilities.h"
#include "MyCustomAsset.h"
#include "SGraphPinCustomAsset.h"

struct UE4COOKBOOKEDITOR_API FMyCustomAssetPinFactory : public FGraphPanelPinFactory
{
  public:
  virtual TSharedPtr<class SGraphPin> CreatePin(class UEdGraphPin* Pin) const override 
  {
    if (Pin->PinType.PinSubCategoryObject == UMyCustomAsset::StaticClass())
    {
      return SNew(SGraphPinCustomAsset, Pin);
    }
    else
    {
      return nullptr;
    }
  };
};
  1. 创建另一个名为SGraphPinCustomAsset的头文件:
#pragma once
#include "SGraphPin.h"

class UE4COOKBOOKEDITOR_API SGraphPinCustomAsset : public SGraphPin
{
  SLATE_BEGIN_ARGS(SGraphPinCustomAsset) {}
  SLATE_END_ARGS()
  void Construct(const FArguments& InArgs, UEdGraphPin* InPin);
  protected:
  virtual FSlateColor GetPinColor() const override { return FSlateColor(FColor::Black); };
  virtual TSharedRef<SWidget> GetDefaultValueWidget() override;
  void ColorPicked(FLinearColor SelectedColor);
};
  1. .cpp文件中实现SGraphPinCustomAsset
#include "UE4CookbookEditor.h"
#include "SColorPicker.h"
#include "SGraphPinCustomAsset.h"

void SGraphPinCustomAsset::Construct(const FArguments& InArgs, UEdGraphPin* InPin)
{
  SGraphPin::Construct(SGraphPin::FArguments(), InPin);
}
TSharedRef<SWidget> SGraphPinCustomAsset::GetDefaultValueWidget()
{
  return SNew(SColorPicker)
  .OnColorCommitted(this, &SGraphPinCustomAsset::ColorPicked);
}

void SGraphPinCustomAsset::ColorPicked(FLinearColor SelectedColor)
{
  UMyCustomAsset* NewValue = NewObject<UMyCustomAsset>();
  NewValue->ColorName = SelectedColor.ToFColor(false).ToHex();
  GraphPinObj->GetSchema()->TrySetDefaultObject(*GraphPinObj, NewValue);
}
  1. #include "Chapter8/MyCustomAssetDetailsCustomization.h"添加到UE4Cookbook编辑器模块实现文件中。

  2. 将以下成员添加到编辑器模块类中:

TSharedPtr<FMyCustomAssetPinFactory> PinFactory;
  1. 将以下内容添加到StartupModule()中:
PinFactory = MakeShareable(new FMyCustomAssetPinFactory());
FEdGraphUtilities::RegisterVisualPinFactory(PinFactory);
  1. 还要将以下代码添加到ShutdownModule()中:
FEdGraphUtilities::UnregisterVisualPinFactory(PinFactory);
PinFactory.Reset();
  1. 编译代码并启动编辑器。

  2. 通过在我的蓝图面板中的函数旁边的加号符号上点击,创建一个新的函数操作步骤...

  3. 添加一个输入参数。操作步骤...

  4. 将其类型设置为MyCustomAsset引用):操作步骤...

  5. 在关卡蓝图的事件图中,放置一个新函数的实例,并验证输入引脚现在具有自定义的可视化器,形式为颜色选择器:操作步骤...

它的工作原理是...

  1. 使用FGraphPanelPinFactory类来自定义蓝图引脚上对象的外观。

  2. 这个类定义了一个虚拟函数:

virtual TSharedPtr<class SGraphPin> CreatePin(class UEdGraphPin* Pin) const
  1. CreatePin函数的作用是创建图针的新的可视化表示。

  2. 它接收一个UEdGraphPin实例。UEdGraphPin包含有关图针所代表的对象的信息,以便我们的工厂类可以根据这些信息做出有根据的决策,确定应该显示哪种可视化表示。

  3. 在函数的实现中,我们检查引脚的类型是否是我们自定义的类。

  4. 我们通过查看PinSubCategoryObject属性来实现这一点,该属性包含一个UClass,并将其与我们自定义资产类关联的UClass进行比较。

  5. 如果引脚的类型符合我们的条件,我们将返回一个指向 Slate 小部件的新共享指针,这是我们对象的可视化表示。

  6. 如果引脚的类型错误,我们返回一个空指针来表示失败状态。

  7. 接下来的类SGraphPinCustomAsset是 Slate 小部件类,它是我们对象的一个字面上的可视化表示。

  8. 它继承自SGraphPin,是所有图针的基类。

  9. SGraphPinCustomAsset类有一个Construct函数,在创建小部件时调用。

  10. 它还实现了父类的一些函数:GetPinColor()GetDefaultValueWidget()

  11. 最后定义的函数是ColorPicked,用于处理用户在自定义引脚中选择颜色的情况。

  12. 在我们自定义类的实现中,我们通过调用Construct的默认实现来初始化我们的自定义引脚。

  13. GetDefaultValueWidget的作用是实际上创建我们类的自定义表示的小部件,并将其返回给引擎代码。

  14. 在我们的实现中,它创建了一个新的SColorPicker实例,我们希望用户能够选择一种颜色,并将该颜色的十六进制表示存储在我们自定义类的FString属性中。

  15. 这个SColorPicker实例有一个名为OnColorCommitted的属性,这是一个可以分配给对象实例上的函数的 slate 事件。

  16. 在返回我们的新SColorPicker之前,我们将OnColorCommitted链接到当前对象上的ColorPicked函数,以便在用户选择新颜色时调用它。

  17. ColorPicked函数接收所选颜色作为输入参数。

  18. 因为当我们关联的引脚没有连接到对象时,该小部件将被使用,所以我们不能简单地将属性设置为所需的颜色字符串。

  19. 我们需要创建我们自定义资产类的一个新实例,我们使用NewObject模板函数来实现这一点。

  20. 这个函数的行为类似于其他章节中讨论的SpawnActor函数,并在返回指针之前初始化指定类的新实例。

  21. 有了一个新的实例,我们可以设置它的ColorName属性。FLinearColors可以转换为FColor对象,它定义了一个ToHex()函数,返回一个十六进制表示所选颜色的FString

  22. 最后,我们需要将我们的新对象实例放置到图中,以便在执行图时引用它。

  23. 为了做到这一点,我们需要访问表示我们的图针对象,并使用GetSchema函数。该函数返回包含我们的针的节点所拥有的图的模式。

  24. 模式包含与图针对应的实际值,并且在图评估过程中是一个关键元素。

  25. 现在我们可以访问模式,我们可以为我们的小部件表示的针设置默认值。如果该针未连接到另一个针,该值将在图评估过程中使用,并且类似于 C++中函数定义期间提供的默认值。

  26. 与本章中我们所做的所有扩展一样,必须进行某种初始化或注册,以告诉引擎在使用其默认内置表示之前,将其委托给我们的自定义实现。

  27. 为了做到这一点,我们需要在编辑器模块中添加一个新的成员来存储我们的PinFactory类实例。

  28. StartupModule期间,我们创建一个引用我们的PinFactory类实例的新共享指针。

  29. 我们将其存储在编辑器模块的成员中,以便以后可以取消注册。然后我们调用FEdGraphUtilities::RegisterVisualPinFactory(PinFactory)来告诉引擎使用我们的PinFactory来创建可视化表示。

  30. ShutdownModule期间,我们使用UnregisterVisualPinFactory取消注册针工厂。

  31. 最后,通过在包含它的共享指针上调用Reset()来删除我们的旧PinFactory实例。

使用自定义详细面板检查类型

默认情况下,派生自UObject的 UAssets 在通用属性编辑器中打开。它的外观如下截图所示:

使用自定义详细面板检查类型

然而,有时您可能希望自定义小部件允许编辑类上的属性。为了方便这一点,虚幻支持详细自定义,这是本教程的重点。

如何操作...

  1. 创建一个名为MyCustomAssetDetailsCustomization.h的新头文件。

  2. 在头文件中添加以下内容:

#include "MyCustomAsset.h"
#include "DetailLayoutBuilder.h"
#include "IDetailCustomization.h"
#include "IPropertyTypeCustomization.h"
  1. 将我们的自定义类定义如下:
class FMyCustomAssetDetailsCustomization : public IDetailCustomization
{
  public:
  virtual void CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) override;
  void ColorPicked(FLinearColor SelectedColor);
  static TSharedRef<IDetailCustomization> FMyCustomAssetDetailsCustomization::MakeInstance()
  {
    return MakeShareable(new FMyCustomAssetDetailsCustomization);
  }
  TWeakObjectPtr<class UMyCustomAsset> MyAsset;
};
  1. 在实现文件中,为CustomizeDetails创建一个实现:
void FMyCustomAssetDetailsCustomization::CustomizeDetails(IDetailLayoutBuilder& DetailBuilder)
{
  const TArray< TWeakObjectPtr<UObject>>& SelectedObjects = DetailBuilder.GetDetailsView().GetSelectedObjects();
  for (int32 ObjectIndex = 0; !MyAsset.IsValid() && ObjectIndex < SelectedObjects.Num(); ++ObjectIndex)
  {
    const TWeakObjectPtr<UObject>& CurrentObject = SelectedObjects[ObjectIndex];
    if (CurrentObject.IsValid())
    {
      MyAsset = Cast<UMyCustomAsset>(CurrentObject.Get());
    }
  }
  DetailBuilder.EditCategory("CustomCategory", FText::GetEmpty(), ECategoryPriority::Important)
  .AddCustomRow(FText::GetEmpty())
  [
    SNew(SVerticalBox)
    + SVerticalBox::Slot()
    .VAlign(VAlign_Center)
    [
      SNew(SColorPicker)
      .OnColorCommitted(this, &FMyCustomAssetDetailsCustomization::ColorPicked)
    ]
  ];
}
  1. 还要创建ColorPicked的定义:
void FMyCustomAssetDetailsCustomization::ColorPicked(FLinearColor SelectedColor)
{
  if (MyAsset.IsValid())
  {
    MyAsset.Get()->ColorName = SelectedColor.ToFColor(false).ToHex();
  }
}
  1. 最后,在.cpp文件中添加以下内容:
#include "UE4CookbookEditor.h"
#include "IDetailsView.h"
#include "DetailLayoutBuilder.h"
#include "DetailCategoryBuilder.h"
#include "SColorPicker.h"
#include "SBoxPanel.h"
#include "DetailWidgetRow.h"
#include "MyCustomAssetDetailsCustomization.h"
  1. 在我们的编辑器模块头文件中,在StartupModule的实现中添加以下内容:
FPropertyEditorModule& PropertyModule = FModuleManager::LoadModuleChecked<FPropertyEditorModule>("PropertyEditor");
PropertyModule.RegisterCustomClassLayout(UMyCustomAsset::StaticClass()->GetFName(), FOnGetDetailCustomizationInstance::CreateStatic(&FMyCustomAssetDetailsCustomization::MakeInstance));
  1. 将以下内容添加到ShutdownModule中:
FPropertyEditorModule& PropertyModule = FModuleManager::LoadModuleChecked<FPropertyEditorModule>("PropertyEditor");
PropertyModule.UnregisterCustomClassLayout(UMyCustomAsset::StaticClass()->GetFName());
  1. 编译代码并启动编辑器。通过内容浏览器创建MyCustomAsset的新副本。

  2. 双击它以验证默认编辑器现在显示您的自定义布局:如何操作...

它是如何工作的...

  1. 通过IDetailCustomization接口执行详细自定义,开发人员可以从中继承,定义一个自定义显示某个类的资产的方式的类。

  2. IDetailCustomization用于允许此过程发生的主要函数是以下函数:

virtual void CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) override;
  1. 在这个函数的实现中,我们使用作为参数传递的DetailBuilder上的方法来获取所有选定对象的数组。然后循环扫描这些对象,以确保至少有一个选定对象是正确类型的。

  2. 通过在DetailBuilder对象上调用方法来自定义类的表示。我们使用EditCategory函数为我们的详细视图创建一个新的类别。

  3. EditCategory函数的第一个参数是我们要操作的类别的名称。

  4. 第二个参数是可选的,并包含类别的可能本地化的显示名称。

  5. 第三个参数是类别的优先级。优先级越高,它在列表中显示得越靠前。

  6. EditCategory作为CategoryBuilder返回对类别本身的引用,允许我们在调用EditCategory时链接其他方法调用。

  7. 因此,我们在CategoryBuilder上调用AddCustomRow(),它会添加一个新的键值对以在类别中显示。

  8. 使用 Slate 语法,我们指定该行将包含一个垂直盒子,其中有一个居中对齐的插槽。

  9. 在插槽内,我们创建一个颜色选择器控件,并将其OnColorCommitted委托绑定到我们的本地ColorPicked事件处理程序上。

  10. 当然,这要求我们定义和实现ColourPicked。它具有以下签名:

void FMyCustomAssetDetailsCustomization::ColorPicked(FLinearColor SelectedColor)
  1. ColorPicked的实现中,我们检查是否有一个选定的资源是正确的类型,因为如果至少有一个选定的资源是正确的,那么MyAsset将被填充为有效值。

  2. 假设我们有一个有效的资源,我们将ColorName属性设置为用户选择的颜色对应的十六进制字符串值。

第九章:用户界面-UI 和 UMG

在本章中,我们将涵盖以下主题:

  • 使用 Canvas 进行绘图

  • 将 Slate 小部件添加到屏幕上

  • 为 UI 创建适应屏幕大小的缩放

  • 在游戏中显示和隐藏一组 UMG 元素

  • 将函数调用附加到 Slate 事件

  • 使用数据绑定与 Unreal Motion Graphics

  • 使用样式控制小部件外观

  • 创建自定义的 SWidget/UWidget

介绍

向玩家显示反馈是游戏设计中最重要的元素之一,这通常涉及到 HUD 或至少游戏中的菜单。

在之前的 Unreal 版本中,有简单的 HUD 支持,允许您在屏幕上绘制简单的形状和文本。然而,从美学角度来看,它在某种程度上有一定的限制,因此,诸如 Scaleform 之类的解决方案变得常见,以解决这些限制。Scaleform 利用 Adobe 的 Flash 文件格式来存储矢量图像和 UI 脚本。然而,对于开发人员来说,它也有自己的缺点,尤其是成本方面-它是一个第三方产品,需要(有时昂贵的)许可证。

因此,Epic 为 Unreal 4 编辑器和游戏内 UI 框架开发了 Slate。Slate 是一组小部件(UI 元素)和一个框架,允许在编辑器中进行跨平台界面。它也可用于游戏中绘制小部件,例如滑块和按钮,用于菜单和 HUD。

Slate 使用声明性语法,允许以本机 C++中的层次结构的 xml 样式表示用户界面元素。它通过大量使用宏和运算符重载来实现这一点。

话虽如此,并不是每个人都想要让他们的程序员设计游戏的 HUD。在 Unreal 3 中使用 Scaleform 的一个重要优势是能够使用 Flash 可视化编辑器开发游戏 UI 的视觉外观,因此视觉设计师不需要学习编程语言。程序员可以单独插入逻辑和数据。这与 Windows Presentation Framework(WPF)的范例相同。

类似地,Unreal 提供了 Unreal Motion Graphics(UMG)。UMG 是 Slate 小部件的可视化编辑器,允许您以可视化方式样式化、布局和动画化用户界面。UI 小部件(或控件,如果您来自 Win32 背景)的属性可以通过蓝图代码(在 UMG 窗口的图形视图中编写)或通过 C++来控制。本章主要涉及显示 UI 元素、创建小部件层次结构和创建可以在 UMG 中进行样式化和使用的基本 SWidget 类。

使用 Canvas 进行绘图

Canvas 是在 Unreal 3 中实现的简单 HUD 的延续。虽然它在发货游戏中并不常用,大多被 Slate/UMG 取代,但在您想要在屏幕上绘制文本或形状时,它非常简单易用。Canvas 绘图仍然广泛用于用于调试和性能分析的控制台命令,例如stat game和其他stat命令。有关创建自己的控制台命令的方法,请参阅第八章,集成 C++和 Unreal Editor

如何操作...

  1. 打开您的.build.cs 文件,并取消注释/添加以下行:
PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" });
  1. 使用编辑器类向导创建一个名为 CustomHUDGameMode 的新 GameMode。如果需要刷新此操作,请参阅第四章,Actors and Components

  2. 在类中添加一个构造函数:

ACustomHUDGameMode();
  1. 将以下内容添加到构造函数实现中:
ACustomHUDGameMode::ACustomHUDGameMode()
:AGameMode()
{
  HUDClass = ACustomHUD::StaticClass();
}
  1. 使用向导创建一个名为 CustomHUD 的新 HUD 子类。

  2. override关键字添加到以下函数:

public:
virtual void DrawHUD() override;
  1. 现在实现函数:
voidACustomHUD::DrawHUD()
{
  Super::DrawHUD();
  Canvas->DrawText(GEngine->GetSmallFont(), TEXT("Test string to be printed to screen"), 10, 10);
  FCanvasBoxItemProgressBar(FVector2D(5, 25), FVector2D(100, 5));
  Canvas->DrawItem(ProgressBar);
  DrawRect(FLinearColor::Blue, 5, 25, 100, 5);
}
  1. 编译您的代码,并启动编辑器。

  2. 在编辑器中,从“设置”下拉菜单中打开“世界设置”面板:操作步骤...

  3. 世界设置对话框中,从游戏模式覆盖列表中选择CustomHUDGameMode操作步骤...

  4. 播放并验证您的自定义 HUD 是否绘制到屏幕上:操作步骤...

工作原理...

  1. 这里的所有 UI 示例都将使用 Slate 进行绘制,因此我们需要在我们的模块和 Slate 框架之间添加依赖关系,以便我们可以访问在该模块中声明的类。

  2. 将自定义 Canvas 绘制调用放入游戏 HUD 的最佳位置是在AHUD的子类中。

  3. 为了告诉引擎使用我们的自定义子类,我们需要创建一个新的GameMode,并指定我们自定义类的类型。

  4. 在自定义游戏模式的构造函数中,我们将新 HUD 类型的UClass分配给HUDClass变量。这个UClass在每个玩家控制器生成时传递给它们,并且控制器随后负责创建它创建的AHUD实例。

  5. 由于我们的自定义GameMode加载了我们的自定义 HUD,我们需要实际创建所述的自定义 HUD 类。

  6. AHUD定义了一个名为DrawHUD()的虚函数,每帧调用该函数以允许我们向屏幕上绘制元素。

  7. 因此,我们重写了该函数,并在实现内部执行绘制操作。

  8. 首先使用的方法如下:

floatDrawText(constUFont* InFont, constFString&InText, float X, float Y, float XScale = 1.f, float YScale = 1.f, constFFontRenderInfo&RenderInfo = FFontRenderInfo());
  1. DrawText需要一个字体来绘制。引擎代码中stat和其他 HUD 绘制命令使用的默认字体实际上存储在GEngine类中,并且可以使用GetSmallFont函数访问,该函数返回一个UFont的实例指针。

  2. 我们使用的剩余参数是要渲染的实际文本以及应该绘制文本的像素偏移量。

  3. DrawText是一个允许您直接传入要显示的数据的函数。

  4. 通用的DrawItem函数是一个访问者实现,允许您创建一个封装有关要绘制的对象的信息的对象,并在多个绘制调用中重用该对象。

  5. 在本示例中,我们创建了一个用于表示进度条的元素。我们将关于框的宽度和高度的所需信息封装到一个FCanvasBoxItem中,然后将其传递给我们的 Canvas 上的DrawItem函数。

  6. 我们绘制的第三个元素是一个填充的矩形。此函数使用在 HUD 类中定义的便利方法,而不是在 Canvas 本身上定义的方法。填充的矩形放置在与我们的FCanvasBox相同的位置,以便它可以表示进度条内的当前值。

将 Slate 小部件添加到屏幕上

之前的示例使用了FCanvas API 来绘制屏幕。然而,FCanvas有一些限制,例如,动画很难实现,绘制图形到屏幕上涉及创建纹理或材质。FCanvas还没有实现任何小部件或窗口控件,使得数据输入或其他形式的用户输入比必要的复杂。本示例将向您展示如何使用 Slate 开始在屏幕上创建 HUD 元素,Slate 提供了许多内置控件。

准备工作

如果您还没有这样做,请将SlateSlateCore添加到您的模块依赖项中(有关如何执行此操作,请参见使用 Canvas 进行绘制的示例)。

操作步骤...

  1. 创建一个名为ACustomHUDPlayerController的新的PlayerController子类。

  2. 在你的新子类中重写BeginPlay virtual方法:

public:
virtual void BeginPlay() override;
  1. 在子类的实现中添加以下代码以覆盖BeginPlay()
void ACustomHUDPlayerController::BeginPlay()
{
  Super::BeginPlay();
  TSharedRef<SVerticalBox> widget = SNew(SVerticalBox)
  + SVerticalBox::Slot()
  .HAlign(HAlign_Center)
  .VAlign(VAlign_Center)
  [
    SNew(SButton)
    .Content()
    [
      SNew(STextBlock)
      .Text(FText::FromString(TEXT("Test button")))
    ]
  ];
  GEngine->GameViewport->AddViewportWidgetForPlayer(GetLocalPlayer(),widget, 1);
}
  1. 如果您现在尝试编译,您将得到一些关于未定义类的错误。这是因为我们需要包含它们的头文件:
#include "SlateBasics.h"
#include "SButton.h"
#include "STextBlock.h"
  1. 创建一个名为SlateHUDGameMode的新的GameMode

  2. 在游戏模式中添加一个构造函数:

ASlateHUDGameMode();
  1. 使用以下代码实现构造函数:
ASlateHUDGameMode::ASlateHUDGameMode()
:Super()
{
  PlayerControllerClass = ACustomHUDPlayerController::StaticClass();
}
  1. 在实现文件中添加以下包含:
#include "CustomHudPlayerController.h"
  1. 在实现文件中添加包含后,编译游戏。

  2. 在编辑器中,从工具栏打开世界设置如何操作...

  3. 世界设置中,覆盖关卡的游戏模式为我们的SlateHUDGameMode如何操作...

  4. 播放关卡,看到新的 UI 显示在屏幕上:如何操作...

它是如何工作的...

  1. 为了在我们的代码中引用 Slate 类或函数,我们的模块必须与SlateSlateCore模块链接,因此我们将它们添加到模块依赖项中。

  2. 我们需要在游戏运行时加载的类中实例化我们的 UI,因此在这个示例中,我们使用我们的自定义PlayerControllerBeginPlay函数中作为创建 UI 的位置。

  3. BeginPlay的实现中,我们使用SNew函数创建一个新的SVerticalBox。我们为我们的框添加一个小部件的插槽,并将该插槽设置为水平和垂直居中。

  4. 在我们使用方括号访问的插槽内,我们创建一个内部有Textblock的按钮。

  5. Textblock中,将Text属性设置为字符串字面值。

  6. 现在创建了 UI,我们调用AddViewportWidgetForPlayer在本地玩家的屏幕上显示此小部件。

  7. 准备好我们的自定义PlayerController后,现在我们需要创建一个自定义的GameMode来指定它应该使用我们的新PlayerController

  8. 在游戏开始时加载自定义的PlayerController,当调用BeginPlay时,我们的 UI 将显示出来。

  9. 在这个屏幕尺寸下,UI 非常小。请参考下一个示例了解如何根据游戏窗口的分辨率进行适当的缩放。

为 UI 创建适应屏幕大小的缩放

如果您按照前面的示例操作,您会注意到当您使用在编辑器中播放时,加载的按钮非常小。

这是由 UI 缩放引起的,该系统允许您根据屏幕大小缩放用户界面。用户界面元素以像素表示,通常是绝对值(按钮应该是 10 个像素高)。

问题在于,如果您使用更高分辨率的面板,10 个像素可能会更小,因为每个像素的大小更小。

准备工作

虚幻引擎中的 UI 缩放系统允许您控制全局缩放修饰符,该修饰符将根据屏幕分辨率缩放屏幕上的所有控件。根据前面的示例,您可能希望调整按钮的大小,以便在较小的屏幕上查看 UI 时其表面大小保持不变。本示例演示了两种不同的方法来改变缩放率。

如何操作...

  1. 创建一个自定义的PlayerController子类,将其命名为ScalingUIPlayerController

  2. 在该类中,覆盖BeginPlay

virtual void BeginPlay() override;
  1. 在该函数的实现中添加以下代码:
Super::BeginPlay();
TSharedRef<SVerticalBox> widget = SNew(SVerticalBox)
+ SVerticalBox::Slot()
.HAlign(HAlign_Center)
.VAlign(VAlign_Center)
[
  SNew(SButton)
  .Content()
  [
    SNew(STextBlock)
    .Text(FText::FromString(TEXT("Test button")))
  ]
];
GEngine->GameViewport->AddViewportWidgetForPlayer(GetLocalPlayer(), widget, 1);
  1. 创建一个名为ScalingUIGameMode的新的GameMode子类,并给它一个默认构造函数:
ScalingUIGameMode();
  1. 在默认构造函数中,将默认的玩家控制器类设置为ScalingUIPlayerController
AScalingUIGameMode::AScalingUIGameMode()
:AGameMode()
{
  PlayerControllerClass = ACustomHUDPlayerController::StaticClass();
}
  1. 这应该给您一个类似于前一个示例的用户界面。请注意,如果您在编辑器中播放,UI 会非常小:如何操作...

  2. 要改变 UI 缩放的速率,我们需要改变缩放曲线。我们可以通过两种不同的方法来实现。

编辑器中的方法

  1. 启动虚幻引擎,然后通过编辑菜单打开项目设置对话框:编辑器中的方法

  2. 用户界面部分,有一个曲线可以根据屏幕的短边来改变 UI 缩放因子:编辑器中的方法

  3. 点击图表上的第二个点或关键点。

  4. 将其输出值更改为 1。编辑器中的方法

配置文件方法

  1. 浏览到项目目录,并查看Config文件夹中的内容:配置文件方法

  2. 在您选择的文本编辑器中打开DefaultEngine.ini

  3. [/Script/Engine.UserInterfaceSettings]部分中找到:

[/Script/Engine.UserInterfaceSettings]
RenderFocusRule=NavigationOnly
DefaultCursor=None
TextEditBeamCursor=None
CrosshairsCursor=None
GrabHandCursor=None
GrabHandClosedCursor=None
SlashedCircleCursor=None
ApplicationScale=1.000000
UIScaleRule=ShortestSide
CustomScalingRuleClass=None
UIScaleCurve=(EditorCurveData=(PreInfinityExtrap=RCCE_Constant,PostInfinityExtrap=RCCE_Constant,Keys=((Time=480.000000,Value=0.444000),(Time=720.000000,Value=1.000000),(Time=1080.000000,Value=1.000000),(Time=8640.000000,Value=8.000000)),DefaultValue=340282346638528859811704183484516925440.000000),ExternalCurve=None)
  1. 在该部分中查找名为UIScaleCurve的关键字。

  2. 在该键的值中,您会注意到许多(Time=x,Value=y)对。编辑第二对,使其Time值为720.000000Value1.000000

  3. 如果您已经打开了编辑器,请重新启动编辑器。

  4. 启动编辑器中的“Play In Editor”预览,以确认您的 UI 现在在PIE屏幕的分辨率下保持可读(假设您使用的是 1080p 显示器,因此 PIE 窗口以 720p 或类似分辨率运行):配置文件方法

  5. 如果您使用新的编辑器窗口预览游戏,还可以看到缩放是如何工作的。

  6. 要这样做,请单击工具栏上播放右侧的箭头。

  7. 选择新的编辑器窗口

  8. 在这个窗口中,您可以使用控制台命令r.setreswidthxheight来改变分辨率,并观察由此产生的变化。

工作原理...

  1. 通常情况下,当我们想要使用自定义的PlayerController时,我们需要一个自定义的GameMode来指定使用哪个PlayerController

  2. 我们创建了一个自定义的PlayerControllerGameMode,并在PlayerControllerBeginPlay方法中放置了一些Slate代码,以便绘制一些 UI 元素。

  3. 因为在 Unreal 编辑器中,主游戏视口通常非常小,所以 UI 最初以缩小的方式显示。

  4. 这旨在使游戏 UI 在较小的分辨率显示器上占用更少的空间,但如果窗口没有被拉伸以适应全屏,可能会导致文本非常难以阅读。

  5. Unreal 存储应在会话之间保持的配置数据,但不一定硬编码到可执行文件中的配置文件中。

  6. 配置文件使用扩展版本的.ini文件格式,这个格式通常用于 Windows 软件。

  7. 配置文件使用以下语法存储数据:

[Section Name]
Key=Value
  1. Unreal 有一个名为UserInterfaceSettings的类,其中有一个名为UIScaleCurve的属性。

  2. UPROPERTY被标记为配置,因此 Unreal 将该值序列化到.ini文件中。

  3. 结果,它将UIScale数据存储在DefaultEngine.ini文件的Engine.UserInterfaceSettings部分中。

  4. 数据使用文本格式存储,其中包含一个关键点列表。编辑TimeValue对会改变或添加新的关键点到曲线中。

  5. 项目设置对话框是直接编辑.ini文件的简单前端界面,对于设计师来说,这是一种直观的编辑曲线的方式。然而,将数据以文本形式存储允许程序员潜在地开发修改UIScale等属性的构建工具,而无需重新编译游戏。

  6. Time指的是输入值。在这种情况下,输入值是屏幕的较窄维度(通常是高度)。

  7. Value是应用于 UI 的通用缩放因子,当屏幕的较窄维度大约等于Time字段中的值的高度时。

  8. 因此,要将 UI 设置为在 1280x720 分辨率下保持正常大小,请将时间/输入因子设置为 720,比例因子设置为 1。

另请参阅

  • 您可以参考 UE4 文档以获取有关配置文件的更多信息

在游戏中显示和隐藏一组 UMG 元素

因此,我们已经讨论了如何将小部件添加到视口中,这意味着它将在玩家的屏幕上呈现。

然而,如果我们希望根据其他因素(例如与某些角色的接近程度、玩家按住某个键或者希望在指定时间后消失的 UI)切换 UI 元素,该怎么办呢?

如何操作...

  1. 创建一个名为ToggleHUDGameMode的新GameMode类。

  2. 覆盖BeginPlayEndPlay

  3. 添加以下UPROPERTY

UPROPERTY()
FTimerHandle HUDToggleTimer;
  1. 最后添加这个成员变量:
TSharedPtr<SVerticalBox> widget;
  1. 在方法体中使用以下代码实现BeginPlay
void AToggleHUDGameMode::BeginPlay()
{
  Super::BeginPlay();
  widget = SNew(SVerticalBox)
  + SVerticalBox::Slot()
  .HAlign(HAlign_Center)
  .VAlign(VAlign_Center)
  [
    SNew(SButton)
    .Content()
    [
      SNew(STextBlock)
      .Text(FText::FromString(TEXT("Test button")))
    ]
  ];
  GEngine->GameViewport->AddViewportWidgetForPlayer(GetWorld()->GetFirstLocalPlayerFromController(), widget.ToSharedRef(), 1);

  GetWorld()->GetTimerManager().SetTimer(HUDToggleTimer, FTimerDelegate::CreateLambda
  ([this] 
  {
    if (this->widget->GetVisibility().IsVisible())
    {
      this->widget->SetVisibility(EVisibility::Hidden);
    }
    else
    {
      this->widget->SetVisibility(EVisibility::Visible);
    }
  }), 5, true);
}
  1. 实现EndPlay
void AToggleHUDGameMode::EndPlay(constEEndPlayReason::Type EndPlayReason)
{
  Super::EndPlay(EndPlayReason);
  GetWorld->GetTimerManager().ClearTimer(HUDToggleTimer);
}
  1. 编译您的代码,并启动编辑器。

  2. 在编辑器中,从工具栏打开World Settings操作步骤...

  3. World Settings中,覆盖关卡的Game Mode为我们的AToggleHUDGameMode操作步骤...

  4. 玩游戏关卡,并验证 UI 每 5 秒切换可见性。

工作原理...

与本章中的大多数其他示例一样,我们使用自定义的GameMode类在玩家的视口上显示单人 UI 以方便操作:

  1. 我们重写BeginPlayEndPlay以便正确处理将为我们切换 UI 的计时器。

  2. 为了实现这一点,我们需要将计时器的引用存储为UPROPERTY,以确保它不会被垃圾回收。

  3. BeginPlay中,我们使用SNew宏创建一个新的VerticalBox,并将一个按钮放在其第一个槽中。

  4. 按钮有Content,可以是其他小部件,如SImageSTextBlock

  5. 在这个示例中,我们将STextBlock放入Content槽中。文本块的内容不重要,只要足够长,我们就能正确看到按钮。

  6. 在初始化小部件层次结构后,我们将根小部件添加到玩家的视口中,以便他们可以看到它。

  7. 现在,我们设置一个计时器来切换小部件的可见性。我们使用计时器来简化这个示例,而不是实现用户输入和输入绑定,但原理是相同的。

  8. 为此,我们获取游戏世界的引用和其关联的计时器管理器。

  9. 有了计时器管理器,我们可以创建一个新的计时器。

  10. 然而,我们需要实际指定计时器到期时要运行的代码。一种简单的方法是使用lambda函数来切换 hud 函数。

  11. lambda是匿名函数。将它们视为文字函数。

  12. 要将lambda函数链接到计时器,我们需要创建一个timer委托。

  13. FTimerDelegate::CreateLambda函数旨在将lambda函数转换为委托,计时器可以在指定的间隔调用它。

  14. lambda需要从其包含对象(即我们的GameMode)访问this指针,以便它可以更改我们创建的小部件实例上的属性。

  15. 为了给它所需的访问权限,我们在lambda声明中使用[]运算符,它将变量封装在lambda中,并在其中可访问。

  16. 然后,花括号将函数体与普通函数声明的方式括起来。

  17. 在函数内部,我们检查小部件是否可见。如果可见,则使用SWidget::SetVisibility隐藏它。

  18. 如果小部件不可见,则使用相同的函数调用将其打开。

  19. 在对SetTimer的其余调用中,我们指定调用计时器的间隔(以秒为单位),并设置计时器循环。

  20. 但是,我们需要小心的是,在两个计时器调用之间,我们的对象可能被销毁,如果对我们的对象的引用被悬空,则可能导致崩溃。

  21. 为了修复这个问题,我们需要移除计时器。

  22. 鉴于我们在BeginPlay中设置了计时器,清除计时器在EndPlay中是有意义的。

  23. EndPlay将在GameMode结束游戏或被销毁时调用,因此我们可以在其实现期间安全地取消计时器。

  24. GameMode设置为默认游戏模式后,UI 将在游戏开始播放时创建,并且计时器委托每 5 秒执行一次,以在小部件之间切换可见性。

  25. 当你关闭游戏时,EndPlay会清除计时器引用,避免任何问题。

将函数调用附加到 Slate 事件

虽然创建按钮很好,但目前,无论用户点击它,屏幕上添加的任何 UI 元素都只是静静地存在。目前我们没有将事件处理程序附加到 Slate 元素,因此鼠标点击等事件实际上不会导致任何事情发生。

准备工作

此示例向您展示如何将函数附加到这些事件,以便在事件发生时运行自定义代码。

操作步骤...

  1. 创建一个名为AClickEventGameMode的新的GameMode子类。

  2. 将以下private成员添加到类中:

private:
TSharedPtr<SVerticalBox> Widget;
TSharedPtr<STextBlock> ButtonLabel;
  1. 添加以下public函数,注意BeginPlay()的重写:
public:
virtual void BeginPlay() override;
FReplyButtonClicked();
  1. .cpp文件中,添加BeginPlay的实现:
void AClickEventGameMode::BeginPlay()
{
  Super::BeginPlay();
  Widget = SNew(SVerticalBox)
  + SVerticalBox::Slot()
  .HAlign(HAlign_Center)
  .VAlign(VAlign_Center)
  [
    SNew(SButton)
    .OnClicked(FOnClicked::CreateUObject(this, &AClickEventGameMode::ButtonClicked))
    .Content()
    [
      SAssignNew(ButtonLabel, STextBlock)
      .Text(FText::FromString(TEXT("Click me!")))
    ]
  ];
  GEngine->GameViewport->AddViewportWidgetForPlayer(GetWorld()->GetFirstLocalPlayerFromController(), Widget.ToSharedRef(), 1);
  GetWorld()->GetFirstPlayerController()->bShowMouseCursor = true;
  GEngine->GetFirstLocalPlayerController(GetWorld())->
  SetInputMode(FInputModeUIOnly().SetLockMouseToViewport(false).SetWidgetToFocus(Widget));
}
  1. 还要为ButtonClicked()添加一个实现:
FReplyAClickEventGameMode::ButtonClicked()
{
  ButtonLabel->SetText(FString(TEXT("Clicked!")));
  returnFReply::Handled();
}
  1. 编译代码并启动编辑器。

  2. 世界设置中覆盖游戏模式为AClickEventGameMode

  3. 在编辑器中预览,并验证 UI 是否显示一个按钮,当您使用鼠标光标单击它时,按钮会从Click Me!更改为Clicked!

工作原理...

  1. 与本章中的大多数示例一样,我们使用GameMode来创建和显示 UI,以最小化需要创建的与示例目的无关的类的数量。

  2. 在我们的新游戏模式中,我们需要保留对我们创建的 Slate 小部件的引用,以便在创建后与它们进行交互。

  3. 因此,我们在GameMode中创建了两个共享指针作为成员数据,一个指向我们 UI 的整体父级或根部件,另一个指向我们按钮上的标签,因为我们将在运行时更改标签文本。

  4. 我们重写BeginPlay,因为它是在游戏开始后创建 UI 的方便位置,并且我们将能够获得对玩家控制器的有效引用。

  5. 我们还创建了一个名为ButtonClicked的函数。它返回FReply,一个指示是否处理了事件的structButtonClicked的函数签名由我们将在下一步中使用的委托FOnClicked的签名确定。

  6. 在我们的BeginPlay实现中,我们首先调用我们要重写的实现,以确保类适当地初始化。

  7. 然后,像往常一样,我们使用SNew函数创建VerticalBox,并向其添加一个居中的插槽。

  8. 我们在该插槽内创建一个新的Button,并向其添加一个值,该值包含在OnClicked属性中。

  9. OnClicked是一个委托属性。这意味着Button将在某个事件发生时广播OnClicked委托(正如在此示例中的名称所暗示的那样,当单击按钮时)。

  10. 要订阅或监听委托,并在事件发生时收到通知,我们需要将委托实例分配给属性。

  11. 我们可以使用标准的委托函数(如CreateUObjectCreateStaticCreateLambda)来实现这一点。其中任何一个都可以工作 - 我们可以绑定UObject成员函数、静态函数、lambda 和其他函数。

注意

请查看第五章,处理事件和委托,了解更多关于委托的内容,以了解我们可以绑定到委托的其他类型的函数。

  1. CreateUObject期望一个指向类实例的指针,并且一个指向该类中定义的成员函数的指针来调用。

  2. 该函数必须具有与委托的签名可转换的签名:

/** The delegate to execute when the button is clicked */
FOnClickedOnClicked;
  1. 如此所示,OnClicked委托类型为FOnClicked - 这就是为什么我们声明的ButtonClicked函数具有与FOnClicked相同的签名的原因。

  2. 通过传入指向此对象实例的指针和要调用的函数的指针,当单击按钮时,引擎将在此特定对象实例上调用该函数。

  3. 设置委托后,我们使用Content()函数,该函数返回对按钮应包含的任何内容的单个插槽的引用。

  4. 然后,我们使用SAssignNew来创建我们按钮的标签,使用TextBlock小部件。

  5. SAssignNew很重要,因为它允许我们使用 Slate 的声明性语法,并且将变量分配给指向层次结构中特定子小部件的指针。

  6. SAssignNew的第一个参数是我们要将小部件存储在其中的变量,第二个参数是该小部件的类型。

  7. 现在,ButtonLabel指向我们按钮的TextBlock,我们可以将其Text属性设置为静态字符串。

  8. 最后,我们使用AddViewportWidgetForPlayer将小部件添加到玩家的视口中,该函数期望LocalPlayer作为参数添加小部件,小部件本身和深度值(较高的值在前面)。

  9. 要获取LocalPlayer实例,我们假设我们在没有分屏的情况下运行,因此第一个玩家控制器将是唯一的控制器,即玩家的控制器。GetFirstLocalPlayerFromController函数是一个方便函数,它只是获取第一个玩家控制器,并返回其本地玩家对象。

  10. 我们还需要将焦点放在小部件上,以便玩家可以点击它,并显示一个光标,以便玩家知道鼠标在屏幕上的位置。

  11. 我们从上一步知道我们可以假设第一个本地玩家控制器是我们感兴趣的控制器,所以我们可以访问它并将其ShowMouseCursor变量更改为true。这将导致光标在屏幕上呈现。

  12. SetInputMode允许我们专注于一个小部件,以便玩家可以与其交互,以及其他与 UI 相关的功能,例如将鼠标锁定到游戏的视口。

  13. 它使用一个FInputMode对象作为其唯一参数,我们可以使用builder模式构造具有我们希望包含的特定元素的对象。

  14. FInputModeUIOnly类是一个FInputMode子类,指定我们希望所有输入事件重定向到 UI 层,而不是玩家控制器和其他输入处理。

  15. builder模式允许我们在将对象实例作为参数发送到函数之前,链接方法调用以自定义对象实例。

  16. 我们链式调用SetLockMouseToViewport(false)来指定玩家的鼠标可以离开游戏屏幕的边界,并使用SetWidgetToFocus(Widget)指定我们的顶级小部件作为游戏应该将玩家输入指向的小部件。

  17. 最后,我们有了我们的实际实现ButtonClicked,我们的事件处理程序。

  18. 当由于点击按钮而运行该函数时,我们将按钮的标签更改为指示它已被点击。

  19. 然后,我们需要返回一个FReply的实例给调用者,以让 UI 框架知道事件已经被处理,并且不要继续向上传播事件。

  20. FReply::Handled()返回设置为指示给框架的FReply

  21. 我们本可以使用FReply::Unhandled(),但这将告诉框架点击事件实际上不是我们感兴趣的事件,它应该寻找其他可能对事件感兴趣的对象。

使用虚幻运动图形进行数据绑定

到目前为止,我们一直将静态值分配给 UI 小部件的属性。然而,如果我们想要在小部件内容或参数(如边框颜色)方面更加动态,怎么办?我们可以使用一个称为数据绑定的原则,将我们的 UI 的属性与更广泛的程序中的变量动态链接起来。

虚幻使用属性系统允许我们将属性的值绑定到函数的返回值,例如。这意味着更改这些变量将自动导致 UI 根据我们的意愿进行更改。

如何做到...

  1. 创建一个名为AAtributeGameMode的新的GameMode子类。

  2. 将以下private成员添加到类中:

private:
TSharedPtr<SVerticalBox> Widget;
  1. 添加以下public函数,注意BeginPlay()的重写:
public:
virtual void BeginPlay() override;
FTextGetButtonLabel() const ;
  1. .cpp文件中添加BeginPlay的实现:
voidAClickEventGameMode::BeginPlay()
{
  Super::BeginPlay();
  Widget = SNew(SVerticalBox)
  + SVerticalBox::Slot()
  .HAlign(HAlign_Center)
  .VAlign(VAlign_Center)
  [
    SNew(SButton)
    .Content()
    [
      SNew(STextBlock)
      .Text( TAttribute<FText>::Create(TAttribute<FText>::FGetter::CreateUObject(this, &AAttributeGameMode::GetButtonLabel)))
    ]
  ];
  GEngine->GameViewport->AddViewportWidgetForPlayer(GetWorld()->GetFirstLocalPlayerFromController(), Widget.ToSharedRef(), 1);
}
  1. 还要为GetButtonLabel()添加一个实现:
FTextAAttributeGameMode::GetButtonLabel() const
{
  FVectorActorLocation = GetWorld()->GetFirstPlayerController()->GetPawn()->GetActorLocation();
  returnFText::FromString(FString::Printf(TEXT("%f, %f, %f"), ActorLocation.X, ActorLocation.Y, ActorLocation.Z));
}
  1. 编译你的代码,并启动编辑器。

  2. 世界设置中覆盖游戏模式为AAtributeGameMode

  3. 请注意,在编辑器中播放时,UI 按钮上的值会随着玩家在场景中移动而改变。

工作原理...

  1. 就像本章中几乎所有其他示例一样,我们首先需要创建一个游戏模式作为我们 UI 的方便宿主。我们以与其他示例相同的方式创建 UI,通过将Slate代码放在游戏模式的BeginPlay()方法中。

  2. 这个示例的有趣之处在于我们如何设置按钮的标签文本的值:

.Text( TAttribute<FText>::Create(TAttribute<FText>::FGetter::CreateUObject(this, &AAttributeGameMode::GetButtonLabel)))
  1. 前面的语法非常冗长,但实际上它所做的事情相对简单。我们将某个值赋给Text属性,该属性的类型是FText。我们可以将TAttribute<FText>赋给该属性,每当 UI 想要确保Text的值是最新的时候,TAttribute Get()方法就会被调用。

  2. 要创建TAttribute,我们需要调用静态的TAttribute<VariableType>::Create()方法。

  3. 该函数期望一个委托的某种描述。根据传递给TAttribute::Create的委托类型,TAttribute::Get()调用不同类型的函数来检索实际值。

  4. 在这个示例的代码中,我们调用了UObject的一个成员函数。这意味着我们知道我们将在某个委托类型上调用CreateUObject函数。

请注意

我们可以使用CreateLambdaCreateStaticCreateRaw来分别在原始的 C++类上调用lambdastaticmember函数。这将为我们提供属性的当前值。

  1. 但是我们想要创建哪种委托类型的实例呢?因为我们在实际变量类型上对TAttribute类进行了模板化,所以我们需要一个委托,该委托的返回值也是以变量类型为模板的。

  2. 也就是说,如果我们有TAttribute<FText>,与之连接的委托需要返回一个FText

  3. 我们在TAttribute中有以下代码:

template<typenameObjectType>
classTAttribute
{
  public:
  /**
   * Attribute 'getter' delegate
   *
   * ObjectTypeGetValue() const
   *
   * @return The attribute's value
   */
  DECLARE_DELEGATE_RetVal(ObjectType, FGetter);
  (…)
}
  1. FGetter委托类型在TAttribute类内声明,因此它的返回值可以在TAttribute模板的ObjectType参数上进行模板化。

  2. 这意味着TAttribute<Typename>::FGetter自动定义了一个具有正确返回类型Typename的委托。

  3. 因此,我们需要创建一个类型和签名为TAttribute<FText>::FGetterUObject绑定的委托。

  4. 一旦我们有了那个委托,我们就可以在委托上调用TAttribute::Create,将委托的返回值与我们的TextBlock成员变量Text关联起来。

  5. 在定义了我们的 UI 并将Text属性、TAttribute<FText>和返回FText的委托绑定之后,我们现在可以将 UI 添加到玩家的屏幕上,以便它可见。

  6. 每一帧,游戏引擎都会检查所有属性,看它们是否与TAttributes相关联。

  7. 如果存在连接,则调用TAttributeGet()函数,调用委托,并返回委托的返回值,以便 Slate 可以将其存储在小部件的相应成员变量中。

  8. 在我们演示这个过程时,GetButtonLabel检索游戏世界中第一个玩家角色的位置。

  9. 然后我们使用FString::Printf将位置数据格式化为可读的字符串,并将其包装在FText中,以便将其存储为TextBlock的文本值。

使用样式控制小部件的外观

到目前为止,在本章中,我们一直在创建使用默认可视化表示的 UI 元素。本示例向您展示了如何在 C++中创建一个可以在整个项目中用作常见外观的样式。

操作步骤如下:

  1. 在你的项目中创建一个新的类头文件。将文件命名为"CookbookStyle.h"

  2. 将以下代码添加到文件中:

#pragma once
#include "SlateBasics.h"
#include "SlateExtras.h"
classFCookbookStyle
{
  public:
  static void Initialize();
  static void Shutdown();
  static void ReloadTextures();
  staticconstISlateStyle& Get();
  staticFNameGetStyleSetName();
  private:
  staticTSharedRef<class FSlateStyleSet> Create();
  private:
  staticTSharedPtr<class FSlateStyleSet>CookbookStyleInstance;
};
  1. 为这个类创建一个相应的实现 cpp 文件,并将以下代码添加到其中:
#include "UE4Cookbook.h"
#include "CookbookStyle.h"
#include "SlateGameResources.h"
TSharedPtr<FSlateStyleSet>FCookbookStyle::CookbookStyleInstance = NULL;
voidFCookbookStyle::Initialize()
{
  if (!CookbookStyleInstance.IsValid())
  {
    CookbookStyleInstance = Create();
    FSlateStyleRegistry::RegisterSlateStyle(*CookbookStyleInstance);
  }
}

voidFCookbookStyle::Shutdown()
{
  FSlateStyleRegistry::UnRegisterSlateStyle(*CookbookStyleInstance);
  ensure(CookbookStyleInstance.IsUnique());
  CookbookStyleInstance.Reset();
}
FNameFCookbookStyle::GetStyleSetName()
{
  staticFNameStyleSetName(TEXT("CookbookStyle"));
  returnStyleSetName;
}
#define IMAGE_BRUSH(RelativePath, ... ) FSlateImageBrush( FPaths::GameContentDir() / "Slate"/ RelativePath + TEXT(".png"), __VA_ARGS__ )
#define BOX_BRUSH(RelativePath, ... ) FSlateBoxBrush( FPaths::GameContentDir() / "Slate"/ RelativePath + TEXT(".png"), __VA_ARGS__ )
#define BORDER_BRUSH(RelativePath, ... ) FSlateBorderBrush( FPaths::GameContentDir() / "Slate"/ RelativePath + TEXT(".png"), __VA_ARGS__ )
#define TTF_FONT(RelativePath, ... ) FSlateFontInfo( FPaths::GameContentDir() / "Slate"/ RelativePath + TEXT(".ttf"), __VA_ARGS__ )
#define OTF_FONT(RelativePath, ... ) FSlateFontInfo( FPaths::GameContentDir() / "Slate"/ RelativePath + TEXT(".otf"), __VA_ARGS__ )

TSharedRef<FSlateStyleSet>FCookbookStyle::Create()
{
  TSharedRef<FSlateStyleSet>StyleRef = FSlateGameResources::New(FCookbookStyle::GetStyleSetName(), "/Game/Slate", "/Game/Slate");
  FSlateStyleSet& Style = StyleRef.Get();
  Style.Set("NormalButtonBrush", 
  FButtonStyle().
  SetNormal(BOX_BRUSH("Button", FVector2D(54,54),FMargin(14.0f/54.0f))));
  Style.Set("NormalButtonText",
  FTextBlockStyle(FTextBlockStyle::GetDefault())
  .SetColorAndOpacity(FSlateColor(FLinearColor(1,1,1,1))));
  returnStyleRef;
}
#undef IMAGE_BRUSH
#undef BOX_BRUSH
#undef BORDER_BRUSH
#undef TTF_FONT
#undef OTF_FONT

voidFCookbookStyle::ReloadTextures()
{
  FSlateApplication::Get().GetRenderer()->ReloadTextureResources();
}
constISlateStyle&FCookbookStyle::Get()
{
  return *CookbookStyleInstance;
}
  1. 创建一个新的游戏模式子类StyledHUDGameMode,并将以下代码添加到其声明中:
#pragma once
#include "GameFramework/GameMode.h"
#include "StyledHUDGameMode.generated.h"
/**
 * 
 */
UCLASS()
class UE4COOKBOOK_API AStyledHUDGameMode : public AGameMode
{
  GENERATED_BODY()
  TSharedPtr<SVerticalBox> Widget;
  public:
  virtual void BeginPlay() override;
};
  1. 同样,实现GameMode
#include "UE4Cookbook.h"
#include "CookbookStyle.h"
#include "StyledHUDGameMode.h"
voidAStyledHUDGameMode::BeginPlay()
{
  Super::BeginPlay();
  Widget = SNew(SVerticalBox)
  + SVerticalBox::Slot()
  .HAlign(HAlign_Center)
  .VAlign(VAlign_Center)
  [
    SNew(SButton)
    .ButtonStyle(FCookbookStyle::Get(), "NormalButtonBrush")
    .ContentPadding(FMargin(16))
    .Content()
    [
      SNew(STextBlock)
      .TextStyle(FCookbookStyle::Get(), "NormalButtonText")
      .Text(FText::FromString("Styled Button"))
    ]
  ];
  GEngine->GameViewport->AddViewportWidgetForPlayer(GetWorld()->GetFirstLocalPlayerFromController(), Widget.ToSharedRef(), 1);
}
  1. 最后,创建一个 54x54 像素的 png 文件,周围有一个边框用于我们的按钮。将其保存到Content|Slate文件夹中,名称为Button.png:!如何做...!如何做...

  2. 最后,我们需要设置我们的游戏模块以在加载时正确初始化样式。在游戏模块的实现文件中,确保它看起来像这样:

class UE4CookbookGameModule : public FDefaultGameModuleImpl
{
  virtual void StartupModule() override
  {
    FCookbookStyle::Initialize();
  };
  virtual void ShutdownModule() override
  {
    FCookbookStyle::Shutdown();
  };
};
  1. 编译代码,并将游戏模式覆盖设置为本章中所做的其他示例中的新游戏模式。

  2. 当你玩游戏时,你会看到你的自定义边框在按钮周围,并且文本是白色而不是黑色。!如何做...

它的工作原理是...

  1. 为了创建可以在多个 Slate 小部件之间共享的样式,我们需要创建一个对象来包含这些样式并使它们保持在范围内。

  2. Epic 为此提供了FSlateStyleSet类。FSlateStyleSet 包含了许多样式,我们可以在 Slate 的声明语法中访问这些样式来为小部件设置皮肤。

  3. 然而,将我们的StyleSet对象的多个副本散布在程序中是低效的。我们实际上只需要一个这样的对象。

  4. 因为FSlateStyleSet本身不是一个单例,也就是说,一个只能有一个实例的对象,我们需要创建一个管理我们的StyleSet对象并确保我们只有一个实例的类。

  5. 这就是为什么我们有FCookbookStyle类的原因。

  6. 它包含一个Initialize()函数,我们将在模块的启动代码中调用它。

  7. Initialize()函数中,我们检查是否有我们的StyleSet的实例。

  8. 如果我们没有一个有效的实例,我们调用私有的Create()函数来实例化一个。

  9. 然后,我们使用FSlateStyleRegistry类注册样式。

  10. 当我们的模块被卸载时,我们需要撤销这个注册过程,然后擦除指针,以防止其悬空。

  11. 现在,我们有了一个类的实例,在模块初始化时通过调用Create()来创建。

  12. 您会注意到,Create被一些具有相似形式的宏包围。

  13. 这些宏在函数之前定义,在函数之后取消定义。

  14. 这些宏使我们能够通过消除我们的样式可能需要使用的所有图像资源的路径和扩展名来简化Create函数中所需的代码。

  15. Create函数内部,我们使用函数FSlateGameResources::New()创建一个新的FSlateStyleSet对象。

  16. New()需要一个样式的名称,以及我们想要在这个样式集中搜索的文件夹路径。

  17. 这使我们能够声明多个指向不同目录的样式集,但使用相同的图像名称。它还允许我们通过切换到其他基本目录中的样式集来简单地为整个 UI 设置皮肤或重新设置样式。

  18. New()返回一个共享引用对象,所以我们使用Get()函数检索实际的FStyleSet实例。

  19. 有了这个引用,我们可以创建我们想要的样式集。

  20. 要将样式添加到集合中,我们使用Set()方法。

  21. Set 期望样式的名称,然后是一个样式对象。

  22. 可以使用builder模式自定义样式对象。

  23. 我们首先添加一个名为"NormalButtonBrush"的样式。名称可以任意选择。

  24. 因为我们想要使用这个样式来改变按钮的外观,所以我们需要使用第二个参数FButtonStyle

  25. 为了根据我们的要求自定义样式,我们使用 Slate 构建器语法,链接我们需要在样式上设置属性的任何方法调用。

  26. 对于这个样式集中的第一个样式,我们只是在按钮没有被点击或处于非默认状态时改变其外观。

  27. 这意味着我们希望在按钮处于正常状态时更改使用的画刷,因此我们使用的函数是SetNormal()

  28. 使用BOX_BRUSH宏,我们告诉 Slate 我们要使用Button.png,这是一个 54x54 像素大小的图像,并且我们要保持每个角的 14 像素不拉伸,以用于九切片缩放。

提示

要更直观地了解九切片缩放功能,请查看引擎源代码中的SlateBoxBrush.h

  1. 在我们的样式集中的第二个样式中,我们创建了一个名为"NormalButtonText"的样式。对于这个样式,我们不想改变样式中的所有默认值,我们只想改变一个属性。

  2. 结果,我们访问默认的文本样式,并使用拷贝构造函数进行克隆。

  3. 使用我们的默认样式的新副本后,我们将文本的颜色更改为白色,首先创建一个线性颜色 R=1 G=1 B=1 A=1,然后将其转换为 Slate 颜色对象。

  4. 配置了我们的样式集并使用我们的两个新样式,然后我们可以将其返回给调用函数Initialize

  5. Initialize存储了我们的样式集引用,并消除了我们创建进一步实例的需要。

  6. 我们的样式容器类还有一个Get()函数,用于检索用于 Slate 的实际StyleSet

  7. 因为Initialize()已经在模块启动时被调用,所以Get()只是返回在该函数内创建的StyleSet实例。

  8. 在游戏模块中,我们添加了实际调用InitializeShutdown的代码。这确保了在我们的模块加载时,我们始终有一个有效的 Slate 样式引用。

  9. 与往常一样,我们创建一个游戏模式作为我们 UI 的主机,并重写BeginPlay以便在游戏开始时创建 UI。

  10. 创建 UI 的语法与我们在之前的示例中使用的完全相同-使用SNew创建VerticalBox,然后使用 Slate 的声明性语法填充该框中的其他小部件。

  11. 重要的是注意以下两行:

.ButtonStyle(FCookbookStyle::Get(), "NormalButtonBrush")
.TextStyle(FCookbookStyle::Get(), "NormalButtonText")
  1. 上述行是我们按钮的声明性语法的一部分,以及作为其标签的文本。

  2. 当我们使用<Class>Style()方法为我们的小部件设置样式时,我们传入两个参数。

  3. 第一个参数是我们实际的样式集,使用FCookbookStyle::Get()检索,第二个参数是一个字符串参数,其中包含我们要使用的样式的名称。

  4. 通过这些小改动,我们重写了小部件的样式,以使用我们的自定义样式,这样当我们将小部件添加到播放器的视口时,它们会显示我们的自定义内容。

创建自定义的 SWidget/UWidget

到目前为止,本章的示例已经向您展示了如何使用现有的基本小部件创建 UI。

有时,开发人员使用组合来方便地将多个 UI 元素收集在一起,例如,定义一个按钮类,自动具有TextBlock作为标签,而不是每次手动指定层次结构。

此外,如果您在 C++中手动指定层次结构,而不是声明由子小部件组成的复合对象,您将无法使用 UMG 将这些小部件作为一组实例化。

准备工作

本示例向您展示了如何创建一个复合的SWidget,其中包含一组小部件,并公开新属性以控制这些子小部件的元素。它还将向您展示如何创建一个UWidget包装器,该包装器将新的复合SWidget类公开给 UMG 供设计师使用。

操作步骤如下:

  1. 我们需要将 UMG 模块添加到我们模块的依赖项中。

  2. 打开<YourModule>.build.cs,并将 UMG 添加到以下位置:

PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore", "UMG" });
  1. 创建一个名为CustomButton的新类,并将以下代码添加到其声明中:
#pragma once
#include "SCompoundWidget.h"
class UE4COOKBOOK_API SCustomButton : public SCompoundWidget
{
  SLATE_BEGIN_ARGS(SCustomButton)
  : _Label(TEXT("Default Value"))
  , _ButtonClicked()
  {}
  SLATE_ATTRIBUTE(FString, Label)
  SLATE_EVENT(FOnClicked, ButtonClicked)
  SLATE_END_ARGS()
  public:
  void Construct(constFArguments&InArgs);
  TAttribute<FString> Label;
  FOnClickedButtonClicked;
};
  1. 在相应的 cpp 文件中实现以下类:
#include "UE4Cookbook.h"
#include "CustomButton.h"
voidSCustomButton::Construct(constFArguments&InArgs)
{
  Label = InArgs._Label;
  ButtonClicked = InArgs._ButtonClicked;
  ChildSlot.VAlign(VAlign_Center)
  .HAlign(HAlign_Center)
  [SNew(SButton)
  .OnClicked(ButtonClicked)
  .Content()
  [
  SNew(STextBlock)
  .Text_Lambda([this] {return FText::FromString(Label.Get()); })
  ]
  ];
}
  1. 创建第二个类,这次基于UWidget,名为UCustomButtonWidget

  2. 添加以下包含:

#include "Components/Widget.h"
#include "CustomButton.h"
#include "SlateDelegates.h"
  1. 在类声明之前声明以下委托:
DECLARE_DYNAMIC_DELEGATE_RetVal(FString, FGetString);
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FButtonClicked);
  1. 添加以下受保护成员:
protected:
TSharedPtr<SCustomButton>MyButton;
virtualTSharedRef<SWidget>RebuildWidget() override;
  1. 还添加以下公共成员:
public:
UCustomButtonWidget();
UPROPERTY(BlueprintAssignable)
FButtonClickedButtonClicked;
FReplyOnButtonClicked();
UPROPERTY(BlueprintReadWrite, EditAnywhere)
FString Label;
UPROPERTY()
FGetStringLabelDelegate;
virtual void SynchronizeProperties() override;
  1. 现在创建UCustomButtonWidget的实现:
#include "UE4Cookbook.h"
#include "CustomButtonWidget.h"
TSharedRef<SWidget>UCustomButtonWidget::RebuildWidget()
{
  MyButton = SNew(SCustomButton)
  .ButtonClicked(BIND_UOBJECT_DELEGATE(FOnClicked, OnButtonClicked));
  returnMyButton.ToSharedRef();
}
UCustomButtonWidget::UCustomButtonWidget()
:Label(TEXT("Default Value"))
{
}

FReplyUCustomButtonWidget::OnButtonClicked()
{
  ButtonClicked.Broadcast();
  returnFReply::Handled();
}
voidUCustomButtonWidget::SynchronizeProperties()
{
  Super::SynchronizeProperties();
  TAttribute<FString>LabelBinding = OPTIONAL_BINDING(FString, Label);
  MyButton->Label = LabelBinding;
}
  1. 通过右键单击内容浏览器,选择用户界面,然后选择小部件蓝图来创建一个新的小部件蓝图:如何操作...

  2. 通过双击打开您的新小部件蓝图

  3. 在小部件面板中找到自定义按钮小部件如何操作...

  4. 将其拖动到主区域中的一个实例。

  5. 选择实例后,在详细信息面板中更改标签属性:如何操作...

  6. 验证您的按钮是否已更改其标签。如何操作...

  7. 现在我们将创建一个绑定,以证明我们可以将任意蓝图函数链接到小部件上的标签属性,从而驱动小部件的文本块标签。

  8. 点击标签属性右侧的绑定,然后选择创建绑定如何操作...

  9. 在现在显示的图表中,放置一个获取游戏时间(以秒为单位)节点:如何操作...

  10. 将获取游戏时间节点的返回值链接到函数中的返回值引脚:如何操作...

  11. 将自动为您插入一个将浮点数转换为字符串的节点:如何操作...

  12. 接下来,通过单击任务栏上的蓝图按钮,然后选择打开关卡蓝图来打开关卡蓝图如何操作...

  13. 将构造小部件节点放入图表中:如何操作...

  14. 选择要生成的小部件类作为我们刚刚在编辑器中创建的新小部件蓝图:如何操作...

  15. 从创建小部件节点上的“拥有玩家”引脚上点击并拖动,然后放置一个“获取玩家控制器”节点:如何操作...

  16. 同样,从创建小部件节点的返回值上点击并拖动,然后放置一个“添加到视口”节点:如何操作...

  17. 最后,将BeginPlay节点链接到创建小部件节点上的执行引脚。如何操作...

  18. 预览游戏,并验证我们在屏幕上显示的小部件是我们的新自定义按钮,其标签绑定到游戏开始后经过的秒数:如何操作...

工作原理...

  1. 为了使用UWidget类,我们的模块需要将 UMG 模块作为其依赖项之一,因为UWidget在 UMG 模块内定义。

  2. 然而,我们需要创建的第一个类是我们实际的SWidget类。

  3. 因为我们想要将两个小部件聚合到一个复合结构中,所以我们将我们的新小部件创建为CompoundWidget子类。

  4. CompoundWidget允许您将小部件层次结构封装为小部件本身。

  5. 在类内部,我们使用SLATE_BEGIN_ARGSSLATE_END_ARGS宏在我们的新SWidget上声明一个名为FArguments的内部结构。

  6. SLATE_BEGIN_ARGSSLATE_END_ARGS之间,使用了SLATE_ATTRIBUTESLATE_EVENT宏。

  7. SLATE_ATTRIBUTE为我们提供的类型创建TAttribute

  8. 在这个类中,我们声明了一个名为_LabelTAttribute,更具体地说,它是一个TAttribute<FString>

  9. SLATE_EVENT允许我们创建成员委托,当小部件内部发生某些事情时可以广播。

  10. SCustomButton中,我们声明了一个具有FOnClicked签名的委托,名为ButtonClicked

  11. SLATE_ARGUMENT是另一个宏,在本示例中未使用,它创建一个带有您提供的类型和名称的内部变量,并在变量名前面添加下划线。

  12. Construct()是小部件在实例化时实现的自我初始化函数。

  13. 您会注意到我们还创建了一个没有下划线的TAttributeFOnClicked实例,这些是我们对象的实际属性,之前声明的参数将被复制到其中。

  14. Construct的实现中,我们检索传递给我们的参数,并将它们存储在此实例的实际成员变量中。

  15. 我们根据传入的内容分配LabelButtonClicked,然后实际创建我们的小部件层次结构。

  16. 我们使用与通常相同的语法,但需要注意的是,我们使用Text_Lambda来设置内部文本块的文本值。

  17. 我们使用lambda函数使用Get()来检索我们的Label TAttribute的值,然后将其转换为FText,并将其存储为我们文本块的Text属性。

  18. 现在我们已经声明了我们的SWidget,我们需要创建一个包装器UWidget对象,将这个小部件暴露给 UMG 系统,以便设计师可以在所见即所得编辑器中使用该小部件。

  19. 这个类将被称为UCustomButtonWidget,它继承自UWidget而不是SWidget

  20. UWidget对象需要引用它拥有的实际SWidget,所以我们在类中放置了一个受保护的成员,将其存储为共享指针。

  21. 声明了一个构造函数,还声明了一个可以在蓝图中设置的ButtonClicked委托。我们还镜像了一个被标记为BlueprintReadWriteLabel属性,以便可以在 UMG 编辑器中设置它。

  22. 因为我们希望能够将按钮的标签绑定到一个委托上,所以我们添加了最后一个成员变量,这是一个返回String的委托。

  23. SynchronizeProperties函数将在我们链接的SWidget上应用在UWidget类中被镜像的属性。

  24. RebuildWidget重新构建与此UWidget关联的本地小部件。它使用SNew来构造我们的SCustomButton小部件的实例,并使用 Slate 声明语法将 UWidget 的OnButtonClicked方法绑定到本地小部件内部的ButtonClicked委托。

  25. 这意味着当本地小部件被点击时,UWidget将通过调用OnButtonClicked来接收通知。

  26. OnButtonClicked通过 UWidget 的ButtonClicked委托重新广播来自本地按钮的点击事件。

  27. 这意味着 UObjects 和 UMG 系统可以在没有对本地按钮小部件的引用的情况下被通知到按钮被点击的事件。我们可以绑定到UCustomButtonWidget::ButtonClicked来接收通知。

  28. OnButtonClicked然后返回FReply::Handled(),表示事件不需要进一步传播。

  29. SynchronizeProperties中,我们调用父类的方法,以确保父类中的任何属性也能正确同步。

  30. 我们使用OPTIONAL_BINDING宏将我们UWidget类中的LabelDelegate委托与TAttribute和本地按钮的标签进行关联。重要的是要注意,OPTIONAL_BINDING宏期望委托被称为NameDelegate,基于宏的第二个参数。

  31. OPTIONAL_BINDING允许通过 UMG 进行的绑定覆盖值,但前提是 UMG 绑定是有效的。

  32. 这意味着当UWidget被告知更新自身时,例如,因为用户在 UMG 中的详细信息面板中自定义了一个值,它将在必要时重新创建本地SWidget,然后通过SynchronizeProperties复制在蓝图/UMG 中设置的值,以确保一切正常工作。

第十章:控制 NPC 的 AI

在游戏中,"人工智能"(AI)的角色非常重要。在本章中,我们将介绍以下用于控制 NPC 角色的 AI 的配方:

  • 放置导航网格

  • 遵循行为

  • 将行为树连接到角色

  • 构建任务节点

  • 使用装饰器进行条件判断

  • 使用周期性服务

  • 使用复合节点-选择器、序列和简单并行

  • 近战攻击者的 AI

介绍

AI 包括游戏的 NPC 以及玩家行为的许多方面。AI 的一般主题包括寻路和 NPC 行为。通常,我们将 NPC 在游戏中的一段时间内所做的选择称为行为。

UE4 中的 AI 得到了很好的支持。编辑器内部提供了许多构造,允许进行基本的 AI 编程。如果引擎内提供的 AI 不符合您的需求,还可以使用 C++进行自定义 AI 编程。

放置导航网格

导航网格(也称为"Nav Mesh")基本上是 AI 控制单位认为可通过的区域的定义(即,"AI 控制"单位被允许进入或穿越的区域)。导航网格不包括如果玩家试图穿过它移动的几何体。

准备就绪

根据场景的几何形状构建导航网格在 UE4 中相当简单。从一些障碍物周围开始,或者使用一个地形。

如何做到这一点...

要构建导航网格,只需执行以下步骤:

  1. 转到"模式" | "体积"。

  2. 将导航网格边界体拖放到视口中。

提示

按下 P 键查看您的导航网格。

  1. 将导航网格的范围扩大到允许使用导航网格的角色可以导航和路径规划的区域。

它是如何工作的...

导航网格不会阻止玩家角色(或其他实体)踩在特定的几何体上,但它可以指导 AI 控制的实体在哪里可以去,哪里不能去。

遵循行为

最基本的 AI 控制跟随行为可以作为一个简单的函数节点使用。您只需要执行以下步骤,就可以让一个 AI 控制的单位跟随一个单位或对象。

准备就绪

准备一个 UE4 项目,其中包含一个简单的地形或一组地形-理想情况下,地形中有一个"死胡同",用于测试 AI 移动功能。在这个地形上创建一个导航网格,以便"AIMoveTo"函数可以按照前面的配方描述的方式工作。

如何做到这一点...

  1. 根据前面的步骤,为您的关卡几何体创建一个导航网格,即"放置导航网格"中所述。

  2. 通过在"类查看器"中找到"Character"类,右键单击它,并选择"创建蓝图类...",创建一个从"Character"派生的蓝图类。

  3. 将您的蓝图类命名为"BP_Follower"。

  4. 双击"BP_Follower"类以编辑其蓝图。

  5. 在"Tick"事件中,添加一个"AIMoveTo"节点,该节点向玩家角色(或任何其他单位)移动,如下所示:如何做到这一点...

它是如何工作的...

如果有可用的导航网格,"AIMoveTo"节点将自动使用导航网格。如果没有可用的导航网格,NPC 单位将不会移动。

还有更多...

如果您不希望单位使用导航网格进行路径规划移动,只需使用"移动到位置或角色"节点即可。

还有更多...

即使在几何体上没有导航网格,"移动到位置或角色"节点也可以工作。

将行为树连接到角色

在任何给定的时间点,"行为树"会选择一个 AI 控制单位要展示的行为。行为树相对简单,但需要进行大量的设置才能运行。您还必须熟悉用于构建"行为树"的组件,以便有效地进行设置。

行为树非常有用,可以定义 NPC 的行为,使其比仅仅向对手移动(如前面的AIMoveTo示例)更加多样化。

准备就绪

设置控制角色的行为树的过程相当复杂。我们首先需要一个Character类派生类的蓝图来进行控制。然后,我们需要创建一个自定义的 AI 控制器对象,该对象将运行我们的行为树来控制我们的近战攻击者角色。我们的蓝图中的AIController类将运行我们的行为树。

准备就绪

行为树本身包含一个非常重要的数据结构,称为黑板。黑板类似于一个黑板,用于存储行为树的变量值。

行为树包含六种不同类型的节点,如下所示:

  1. 任务:任务节点是行为树中的紫色节点,包含要运行的蓝图代码。这是 AI 控制的单位必须要做的事情(代码方面)。任务必须返回truefalse,取决于任务是否成功(通过在末尾提供FinishExecution()节点)。准备就绪

  2. 修饰器:修饰器只是节点执行的布尔条件。它检查一个条件,通常在选择器或序列块中使用。准备就绪

  3. 服务:在每次执行时运行一些蓝图代码。这些节点的执行间隔是可调节的(可以比每帧执行慢,例如每 10 秒执行一次)。您可以使用这些节点查询场景更新,或者追逐新的对手等等。黑板可以用来存储查询到的信息。服务节点在末尾没有FinishExecute()调用。在前面的图表中的序列节点中有一个示例服务节点。

  4. 选择器:从左到右运行所有子树,直到遇到成功。遇到成功后,执行返回到树的上层。

  5. 序列:从左到右运行子树,直到遇到失败。遇到失败后,执行返回到树的上层。准备就绪

注意

选择器节点尝试执行节点,直到成功(然后返回),而序列节点执行所有节点,直到遇到失败(然后返回)。

请记住,如果您的任务没有调用FinishExecute(),选择器和序列将无法连续运行多个任务。

  1. 简单并行:在并行运行一个任务(紫色)和一个子树(灰色)。准备就绪

如何操作...

  1. 首先,在 UE4 中为您的近战单位创建一个蓝图。您可以通过从Character派生一个自定义蓝图来实现。要这样做,请转到类查看器,输入Character,然后右键单击。从出现的上下文菜单中选择创建蓝图...,并将您的蓝图类命名为BP_MeleeCharacter

  2. 要使用行为树,我们需要首先为我们的Character类派生类设置一个自定义 AI 控制器。转到内容浏览器,从AIController类派生一个蓝图,确保首先关闭过滤器 | 仅限角色

注意

非 actor 类的派生类默认情况下不显示在类查看器中!要显示AIController类,您需要转到过滤器菜单并取消选中仅限角色菜单选项。

  1. 通过在内容浏览器中右键单击并选择人工智能 | 行为树人工智能 | 黑板来创建您的行为树和黑板对象。

  2. 打开行为树对象,在详细信息面板的黑板资产下,选择您创建的黑板。黑板包含用于行为树的键和值(命名变量)。如何操作...

  3. 打开您的BP_AIMeleeController类派生类并转到事件图。在事件 BeginPlay下,选择并添加一个运行行为树节点到图表中。在BTAsset下,确保选择您的BehaviorTree_FFA_MeleeAttacker资源。操作步骤...

工作原理...

行为树连接到 AI 控制器,而 AI 控制器连接到角色的蓝图。我们将通过在图表中输入任务和服务节点来通过行为树控制Character的行为。

构建任务节点

任务节点类似于函数块。您构建的每个任务节点都将允许您将一些蓝图代码捆绑在一起,以在行为树中满足某些条件时执行。

任务有三个不同的事件:接收 Tick(带有 AI 版本),接收执行(AI)和接收中止(AI)。您可以在任务的蓝图中响应这三个事件中的任何一个。通常,您应该响应任务的接收执行(AI 版本)。

准备工作

要创建一个任务节点,您应该已经准备好一个行为树,并将其附加到适当的 AI 控制器和蓝图角色上(参见前面的示例)。

操作步骤...

  1. 要在任务节点中构建可执行的蓝图代码,您必须从我们的行为树蓝图编辑器的菜单栏中选择新任务。从出现的下拉菜单中,选择以BTTask_BlueprintBase为基础的新任务操作步骤...

提示

与行为树或黑板创建不同,没有直接从内容浏览器创建新任务的方法。

  1. 双击打开刚刚创建的行为树任务以进行编辑。覆盖任何可用事件(在我的蓝图选项卡下的函数子标题中列出):

  2. 接收 Tick AI:行为树任务的Tick事件的 AI 版本。当您需要任务与包含它的角色一起进行Tick时,应该覆盖此函数。如果您只希望任务在行为树调用它时执行(而不是在游戏引擎进行 Tick 时执行),请不要覆盖此函数。

  3. 接收执行 AI:您要覆盖的主要函数。接收执行 AI 允许您在从行为树图表中调用任务节点时运行一些蓝图代码。

  4. 接收中止 AI:当任务被中止时调用的行为树任务中止。当蓝图图表中的FinishAbort()节点调用时,应该覆盖此函数。

提示

前面的函数还有非 AI 版本,它们只是参数有所不同:在*AI版本中,所有者对象被强制转换为Pawn,并且有一个所有者控制器传递给事件调用。

使用装饰器进行条件判断

装饰器是一种允许您在评估另一个节点时输入条件表达式的节点。它们的命名相当奇怪,但它们被称为装饰器,因为它们倾向于为执行节点添加执行条件。例如,在下面的图表中,只有在满足装饰器条件时才会执行MoveTo函数:

使用装饰器进行条件判断

UE4 附带了几个预打包的装饰器,包括黑板(变量检查),比较黑板条目锥体检查冷却时间路径是否存在等等。在本示例中,我们将探索使用其中一些条件来控制行为树的不同分支的执行。

准备工作

只有在现有行为树的菜单栏中才能创建装饰器。

准备工作

注意

新装饰器按钮位于现有行为树的菜单栏中,因此要找到它,您必须打开现有的行为树

操作步骤...

  1. 在现有行为树的菜单栏中,选择新装饰器。以现有蓝图BTDecorator_BlueprintBase为基础。

  2. 组装您的蓝图图表,确定装饰器的条件在PerformConditionCheck函数覆盖下是否成功。如何操作...

  3. 装饰器的内部检查是否跟随黑板中的目标是否在某个半径的边界球内。如果装饰器的条件满足(并且依赖于装饰器的块执行),则返回true,否则返回false(并且依赖于装饰器的块不执行)。

工作原理...

装饰器就像if语句一样;唯一的区别是它们在行为树中直接在它们下面放置一个条件来执行节点。

使用周期性服务

服务是包含要定期执行的蓝图代码的节点。服务与任务非常相似,但它们没有FinishExecute()的调用。

准备工作

将服务添加到行为树中对于周期性检查非常重要,例如检查是否有任何新的敌方单位在范围内,或者当前目标是否离开焦点。您可以创建自己的服务。在本教程中,我们将组装一个服务,该服务将检查您正在跟随的对手是否仍然是可见锥体内最近的对手。如果不是,则对手将更改。

服务节点有四个主要事件(除了 Tick):

  1. 接收激活 AI:当行为树启动并且节点首次激活时触发。

  2. 接收搜索开始 AI:当行为树进入底层分支时触发。

  3. 接收 Tick AI:在调用服务的每一帧触发。大部分工作在这里完成。

  4. 接收停用 AI:当行为树关闭并且节点停用时触发。

如何操作...

  1. 首先,通过行为树菜单栏中的新服务按钮将新服务添加到行为树中:如何操作...

  2. 将您的服务命名为描述其功能的名称,例如BehaviorTree_Service_CheckTargetStillClosest

  3. 双击服务以开始编辑其蓝图。

  4. 在编辑器中,添加一个接收 Tick AI 节点,并对您需要的黑板进行任何更新。

工作原理...

服务节点在一些规律的时间间隔(可以有偏差选项)执行一些蓝图代码。在服务节点内部,通常会更新您的黑板。

使用复合节点 - 选择器、序列和简单并行

复合节点形成行为树中的树节点,并包含多个要在其中执行的内容。有三种类型的复合节点:

  • 选择器:从左到右遍历子节点,寻找成功的节点。如果一个节点失败,它会尝试下一个节点。当成功时,节点完成,我们可以返回树。

  • 序列:从左到右执行,直到节点失败。如果节点成功,则执行下一个节点。如果节点失败,则返回树。

  • 简单并行:将单个任务(紫色)与某个子树(灰色)并行执行。

准备工作

使用复合节点非常简单。您只需要一个行为树就可以开始使用它们。

如何操作...

  1. 在行为树图中的空白处右键单击。

  2. 选择复合 | 选择器或复合 | 序列

  • 选择器:按顺序执行所有任务,直到成功执行一个任务。

  • 序列:按顺序执行所有任务,直到一个任务失败。

  1. 根据需要将一系列任务或其他复合节点附加到节点上。

近战攻击者的 AI

我们可以使用行为树构建具有近战攻击行为的 NPC。近战攻击者将具有以下行为:

  1. 每 10 秒搜索最佳对手进行攻击。最佳对手是范围内最近的对手。我们将使用一个服务来实现这一点。将我们正在攻击的对手记录在近战攻击者的行为树黑板中。

  2. 朝着我们正在攻击的对手移动(由黑板指示)。

  3. 如果我们与对手的距离小于AttackRadius单位,则每隔AttackCooldown秒对正在攻击的对手造成伤害。

提示

这只是使用BehaviorTree攻击对手的一种方式。你会发现你也可以在近战攻击者的攻击动画中进行攻击,在这种情况下,你只需在接近对手的AttackRadius范围内指示播放动画。

准备工作

准备一个近战攻击者角色的蓝图。我称之为BP_Melee。准备BP_Melee角色的 AI 控制器,以使用我们接下来将创建的新行为树。

如何操作...

  1. 从根节点开始,如果失败则立即返回。在其中创建一个名为BehaviorTree_Service_FindOpponent的新序列节点。将节点的间隔设置为 10 秒。

  2. 按照以下步骤构建BehaviorTree_Service_FindOpponent节点:如何操作...

  3. 在另一个行为树节点中,指示每帧朝着跟随目标移动:如何操作...

  4. 最后,当玩家在对手的AttackRadius范围内时,我们希望对对手造成伤害。当玩家在AttackRadius范围内时,你可以开始播放攻击动画(这可能会触发对对手的伤害事件),运行一个伤害服务(每隔AttackCooldown秒),或者如下截图所示简单地进行冷却对对手造成伤害如何操作...

第十一章:自定义材质和着色器

UE4 中的材质定义和创建工具非常出色,更不用说其实时渲染性能了。当您看到您的第一个闪闪发光的金色着色器时,您会对 UE4 的材质着色能力感到惊讶,这是通过一些数学计算实现的。我们将通过以下教程向您展示如何使用这些工具:

  • 使用基本材质修改颜色

  • 使用材质修改位置

  • 通过自定义节点的着色器代码

  • 材质函数

  • 着色器参数和材质实例

  • 闪烁

  • 叶子和风

  • 与观察角度有关的反射

  • 随机性-柏林噪声

  • 给景观着色

介绍

在计算机图形学中,着色器用于给某物上色。传统上,着色器之所以被称为着色器,是因为它们根据原始颜色和光源位置定义了物体的阴影。

现在,着色器不再被认为是为对象提供阴影,而是提供纹理和最终颜色。

介绍

注意

着色器是关于确定物体的最终颜色的,给定光源、几何位置和初始颜色(包括纹理,以及更昂贵的材质属性)。

着色器有两种类型:顶点着色器和像素着色器。

  • 顶点着色器:顶点(网格中的点)的颜色,并且从一个三维点平滑着色到另一个三维点。

  • 像素着色器:像素(屏幕上的点)的颜色。使用一些简单的数学计算来计算像素(也称为片段)的三维物理位置。

在 UE4 中,我们将着色器称为材质。材质将顶点和片段处理管线抽象为可编程块函数,因此您无需考虑 GPU 或编码即可获得所需的图形输出。您只需以块和图片的形式思考。您可以构建材质并构建 GPU 着色功能,而无需编写一行高级着色语言HLSL)、OpenGL 着色语言GLSL)或 Cg(用于图形)代码!

提示

您通常会听到三种主要的 GPU 编程语言:HLSL、GLSL 和 Cg。GLSL 是 OpenGL 的 GPU 编程语言,而 HLSL 是微软的产品。在 90 年代和 21 世纪的第一个十年中,Cg 诞生了,试图将所有 GPU 编程统一起来。Cg 仍然很受欢迎,但 GLSL 和 HLSL 也仍然广泛使用。

使用基本材质修改颜色

材质的主要用途是使表面呈现您想要的颜色。在您的场景中,您将拥有光源和表面。表面上涂有反射和折射光线的材质,您可以通过相机的眼睛看到。材质的基本操作是修改表面的颜色。

提示

不要忽视调整光源以使材质看起来符合您的期望的重要性!

熟悉材质编辑器需要一些练习,但一旦您熟悉了它,您可以用它做出令人惊叹的事情。在本教程中,我们将只使用一些非常基本的功能来构建一个木质纹理材质。

提示

纹理与材质的区别:请记住,纹理和材质这两个术语之间有很大的区别。纹理只是一个图像文件(例如一张名为wood.png的照片);而材质则是一组纹理、颜色和数学公式的组合,用于描述表面在光线下的外观。材质将考虑表面的属性,如颜色吸收、反射和光泽度,而纹理只是一组有色像素(或者 GPU 称之为纹素)。

着色器的编程方式与普通的 C++代码相同,只是限制更多。有几种参数类型可供选择。其中大多数将是浮点数或以向量格式排列的浮点数包(floatfloat2float3float4)。对于位置和颜色等内容,您将使用float3float4;对于纹理坐标等内容,您将使用float2

准备工作

您需要一个干净的 UE4 项目,将其中放置您的新材质。在 UE4 项目中安装来自 UE4 市场(Epic Games Launcher 应用程序)的GameTexture Materials包。它包含我们在本教程中需要的一些必需纹理。您还需要一个简单的几何体来显示着色器的结果。

如何操作...

  1. 要创建一个基本材质,在内容浏览器中右键单击,并创建一个材质(在前四个基本资产元素中可用)。如何操作...

  2. 为您的材质命名(例如GoldenMaterial),然后双击它进行编辑。

  3. 欢迎来到材质编辑器:如何操作...

  4. 您可以通过右侧的材质输出节点来判断它是材质编辑器。左侧是一个 3D 渲染的球体,展示了您的材质的外观。材质最初是一种类似煤炭的黑色半光泽材质。我们可以调整所有材质参数,从像太阳一样发光的材质,到水,或者到单位装甲的纹理。让我们从调整材质的输出颜色开始,创建一个金色的金属材质。

  5. 通过右键单击材质编辑器窗口中的任何空白处,并选择Constant3Vector(表示 RGB 颜色)将基础颜色更改为黄色。通过双击节点并拖动颜色样本的值来调整颜色。将 Constant3Vector 的输出连接到基础颜色,等待左侧的 3D 图片重新加载以显示您的新材质外观。将 Constant3Vector 的输出连接到基础颜色,使材质呈现黄色,如下图所示:如何操作...

  6. 通过将一个常量值附加到金属输入并将其设置为 1,为所有通道选择一个金属度级别。1 表示非常金属,0 表示完全不金属(因此看起来像下一个截图中显示的材质一样塑料)。如何操作...

  7. 为材质选择一个高光值,同样在 0 到 1 之间。高光材质是有光泽的,而非高光材质则没有。

  8. 为材质选择一个粗糙度值。粗糙度指的是镜面高光的扩散程度。如果粗糙度很高(接近 1.0),则表面类似于黏土,几乎没有镜面高光。镜面高光在 0.7 或 0.8 附近的值附近呈现出较宽的形状。当粗糙度接近 0 时,镜面高光非常锐利而细小(极其光亮/镜面般的表面)。

如何操作...

注意

左侧的材质的粗糙度为 0,右侧的材质的粗糙度为 1。

  1. 通过单击并拖动材质到您想要应用材质的模型网格上,将材质应用于场景中的对象。或者,通过名称在详细信息面板中选择一个模型网格组件和您创建的新材质。

  2. 最后,在场景中创建一个光源以进一步检查材质的响应属性。没有光源,每个材质都会显示为黑色(除非它是自发光材质)。通过模式 | 灯光添加一个光源。如何操作...

使用材质修改位置

不常见的是使用材质来修改对象的位置。这通常在水着色器等方面使用。我们使用材质输出中的世界位置偏移节点来实现这一点。

使用材质修改位置

我们可以使用一些 GPU 数学来调制顶点的输出位置。这样做可以显著减轻 CPU 渲染逼真水体的负担。

准备工作

在你的世界中创建一个几何体。构建一个名为Bob的新着色器,我们将编辑它以产生一个简单的上下浮动的运动效果,用于渲染使用该材质的对象。

操作步骤...

  1. 在你的新材质(名为Bob)中,右键单击并添加TexcoordTime Input节点。

  2. 通过对sin()函数调用级联Texcoord(用于空间)和Time Input节点的总和,创建一些波浪位移。将sin()函数的输出乘以并作为 Z 输入传递给World Displacement操作步骤...

注意

给出在Chapter11代码中的简单水体着色器的一部分,它产生位移。

  1. Tessellation | D3D11Tessellation Mode下选择PN Triangles,并将材质中的Tessellation Multiplier设置为 1.0。操作步骤...

提示

通常情况下,UE4 着色器中无法同时使用高光和半透明效果。然而,表面每像素(实验性,功能有限)光照模式允许你同时启用两者。除了选择这种光照模式外,你还必须记住确保按下```cpp and type r.ForwardLighting 1 in the Stats console window.

Shader code via Custom node

If you prefer code to diagrammatic blocks, you're in luck. You can write your own HLSL code to deploy to the GPU for the shading of some vertices in your project. We can construct Custom nodes that simply contain math code working on named variables to perform some generic computation. In this recipe, we'll write a custom math function to work with.

Getting ready

You need a material shader, and a general mathematical function to implement. As an example, we'll write a Custom node that returns the square of all inputs.

How to do it...

  1. In order to create a custom material expression, simply right-click anywhere on the canvas, and select Custom.How to do it...

  2. With your new Custom block selected, go to the Details panel on the left side of your Material Editor window (choose Window | Details if your Details panel is not displayed).

  3. Under Description, name your Custom block. For example, Square3, because we plan to square three float inputs and return a float3.

  4. Click the + icon as many times you need to generate as many inputs as you need to serve. In this case, we're going to serve three float inputs.

  5. Name each input. We've named ours x, y, and z in the diagram that follows. To use each input in the calculation, you must name it.

  6. Select the output type. Here we chose to output a float3.

  7. Enter the computation in the Code section at the top using the named variables you have created. The code we return here is as follows:

    
    

return float3( xx, yy, z*z );


![操作步骤...](https://gitee.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/ue4-scp-cpp-cb/img/00272.jpeg)

### 提示

这样做的作用是构建一个 3 个浮点数的向量,并将*X*的平方返回到`x`值中,将*Y*的平方返回到`y`值中,将*Z*的平方返回到`z`值中。

为了返回向量类型的*X*、*Y*、*Z*分量的不同值,我们必须返回对`float3`或`float4`构造函数的调用。如果你不返回向量类型,你可以只使用一个`return`语句(不调用`float`构造函数)。

## 工作原理...

自定义节点实际上只是一段 HLSL 代码。任何有效的 HLSL 代码都可以在代码文本字段中使用。顶点或像素着色器程序中有几个标准输入。这些标准输入已经定义了很长时间,它们是你可以用来改变几何体渲染方式的参数。

![工作原理...](https://gitee.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/ue4-scp-cpp-cb/img/00273.jpeg)

HLSL 和 Cg 有一个称为语义的概念,它将一种具体的类型与一个浮点数关联起来。这样做是为了外部调用着色器的程序在调用顶点或像素着色程序时知道在哪里放置哪个输入。

在下面的 Cg 函数签名中,除了是一个`float`变量之外,`inPosition`在语义上是一个`POSITION`类型的变量,`inTexcoord`是一个`TEXCOORD`类型的变量,`inColor`是一个`COLOR`类型的变量。在着色器内部,你可以将这些变量用于任何你想要的目的,语义只是为了将正确的输入路由到正确的变量(以确保颜色通过`COLOR`类型的变量输入,否则我们将不得不跟踪参数的指定顺序或其他操作!)

函数的输出参数指定了如何解释着色器的输出。解释仅适用于程序的输出数据的接收者(渲染管线中的下一步)。在着色器程序内部,你知道你只是将一堆浮点数写入着色器管线。没有什么禁止你在着色器内部混合不同类型的语义。一个`COLOR`语义变量可以乘以一个`POSITION`语义输入,并作为`TEXCOORD`语义输出发送出去,如果你愿意的话。

# 材质函数

一如既往,**模块化**是编程中的最佳实践之一。材质着色器也不例外:如果你的着色器块是模块化的,并且可以被封装并标识为命名函数,那将更好。这样,不仅你的着色器块更清晰,而且它们还可以在多个材质着色器中重复使用,甚至可以导出到本地 UE4 库中以供将来在其他项目中使用。

## 准备工作

可以将可重用的着色器功能块从自定义材质着色器程序中分离出来。在本示例中,我们将编写一个简单的函数系列——`Square`、`Square2`、`Square3`和`Square4`——来对输入值进行平方。通过打开 UE4 项目并导航到**内容浏览器**,准备好在本教程中执行工作。

## 如何操作...

1.  在**内容浏览器**中右键单击,然后选择**Materials & Textures** | **Material Function**。![如何操作...](https://gitee.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/ue4-scp-cpp-cb/img/00274.jpeg)

1.  将您的**材质函数**命名为`Square`。

1.  双击**材质函数**。

1.  一旦打开**材质函数**,通过在材质编辑器的空白画布空间中的任何位置左键单击,取消选择**输出结果**节点。查看**详细信息**面板,并注意函数对 UE4 库的暴露是可选的:![如何操作...](https://gitee.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/ue4-scp-cpp-cb/img/00275.jpeg)

1.  当在**材质函数**编辑器屏幕中没有选择节点时,**详细信息**面板中会出现**暴露到库**复选框。

1.  在**材质函数**编辑器的空白处右键单击,然后选择**输入**。为您的输入命名。请注意,**输入**节点仅在**材质函数**编辑器中可用,而不在普通的材质编辑视图中可用。

1.  从任何常规材质中,通过以下方式之一调用您的函数:

1.  在空白处右键单击,然后选择`MaterialFunction`,然后从下拉菜单中选择您的`MaterialFunction`。

1.  右键单击并输入您的**材质函数**的名称(这要求您先前已经暴露了您的**材质函数**)。

1.  如果您不想将您的**材质函数**暴露给 UE4 库,则必须使用`MaterialFunction`块来调用您的自定义函数。

1.  在**材质函数**编辑器的任何位置右键单击,然后选择**输出**。

## 它是如何工作的...

**材质函数**是您可以创建的最有用的块之一。通过使用它们,您可以将着色器代码模块化,使其更整洁、紧凑和可重用。

## 还有更多...

将功能迁移到着色器库是一个好主意。通过在着色器的根部选择**暴露到库**,您可以使自定义函数出现在函数库中(前提是在材质编辑器窗口中没有选择任何内容)。

在开发**材质函数**时,有时将材质预览节点更改为输出节点以外的节点会很有帮助。通过右键单击任何节点的输出插孔并选择**开始预览节点**来预览特定节点的输出。

![还有更多...](https://gitee.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/ue4-scp-cpp-cb/img/00276.jpeg)

材质编辑器左上角的窗口现在将显示您正在预览的节点的输出。此外,如果您正在预览的节点不是最终输出节点,则会在您正在预览的节点上添加文本**正在预览**。确保在材质编辑器顶部的菜单栏中启用了**实时预览**。通常,您希望预览最终输出。

# 着色器参数和材质实例

着色器的参数将成为该着色器的变量输入。您可以配置标量或矢量作为您的着色器的输入参数。UE4 中的某些材质预先编程了暴露的材质参数。

## 准备工作

为了设置着色器的参数,您首先需要一个带有您想要使用变量修改的内容的着色器。一个好的用变量修改的东西是角色的服装颜色。我们可以将服装的颜色作为着色器参数暴露出来,然后将其与服装颜色相乘。

## 如何操作...

1.  构建一个新的材质。

1.  在材质中创建一个`VectorParameter`。给参数一个名称,例如`Color`。给它一个默认值,例如蓝色或黑色。![如何操作...](https://gitee.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/ue4-scp-cpp-cb/img/00277.jpeg)

1.  关闭材质。

1.  在**内容浏览器**中,右键单击具有参数的材质,并选择**创建材质实例**。

1.  双击您的材质实例。勾选您的`VectorParameter`名称旁边的复选框,完成!您的`VectorParameter`可以自定义,而不会进一步影响材质的基本功能。![如何操作...](https://gitee.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/ue4-scp-cpp-cb/img/00278.jpeg)

1.  此外,如果您更改了材质的基本功能,材质实例将继承这些更改,而无需进行任何进一步的配置。

## 工作原理...

材质参数允许您编辑发送到材质的变量的值,而无需编辑材质本身。此外,您还可以轻松地从 C++代码更改材质实例的值。这对于诸如团队颜色之类的事物非常有用。

# 闪烁

通过在 UE4 材质编辑器中使用标准节点,可以轻松访问一些着色器功能。您可以构建一些漂亮的斑点效果,例如我们在下一个示例中展示的闪闪发光的金色着色器。这个示例的目的是让您熟悉材质编辑器的基本功能,以便您可以学会构建自己的材质着色器。

## 准备工作

创建一个您想要发光的资产(例如一个宝箱),或者打开`Chapter11`的源代码包以找到`treasureChest.fbx`模型。

我们要做的是在物体上移动一个厚度为*W*的平面。当平面经过几何体时,发射颜色通道被激活,从而在宝藏上创建出闪烁效果。

我们公开了几个参数来控制闪烁,包括**速度**,**周期**(闪烁之间的时间),**宽度**,**增益**,**平面方向**,最后是**颜色**。

## 如何操作...

1.  通过在**内容浏览器**中右键单击并选择**材质**来创建一个新的材质。

1.  按照以下图像所示添加输入参数,引入一个`Time`输入,并通过使用时间周期调用`Fmod`使其成为周期性的:![如何操作...](https://gitee.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/ue4-scp-cpp-cb/img/00279.jpeg)

1.  使用周期的`Fmod`将使时间遵循锯齿形模式。读取的时间值不会超过**周期**,因为我们将使用`fmod`操作将其保持为 0。

1.  在一个单独的文件中提供`OnPlane`函数。`OnPlane`函数使用平面方程*Ax + By + Cz + D = 0*来确定输入点是否在平面上。将`LocalPosition`坐标传递到`OnPlane`函数中,以确定在给定帧中,是否应该在几何体中用发光突出显示此部分。

## 工作原理...

一个想象中的光平面以指定的速度通过几何体。光平面每隔**周期**秒从一个边界框的角落开始,沿着**平面方向**指定的方向移动。当平面随时间向前移动时,它总是从盒子的角落开始,当平面通过整个体积时,它将通过整个体积。

# 树叶和风

在这个示例中,我们将编写一个简单的粒子着色器,演示如何在风中创建树叶。我们可以使用一个**粒子发射器**结合一个材质着色器来实现这一点,通过"着色"我们的树叶,使它们看起来像在风中飘动。

![树叶和风](https://gitee.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/ue4-scp-cpp-cb/img/00280.jpeg)

## 准备工作

首先,您需要一个树叶纹理以及一个放置落叶的场景。在`Chapter11`代码包中,您会找到一个名为`LeavesAndTree`的场景,其中包含一个落叶树,您可以使用它。

## 如何操作...

1.  通过在**内容浏览器**中右键单击并选择**粒子系统**来创建一个新的粒子发射器。

1.  通过在**内容浏览器**中右键单击并选择**材质**来构建一个新的材质着色器。您的叶子材质应该包含一个叶子的纹理在`BaseColor`组件中。我们将在后面的步骤中编辑叶子的**世界位置**,以表示由风引起的运动中的抖动。

1.  添加一些参数来修改树叶粒子发射器:

1.  **生成**应该有一个很高的速率,大约为 100。

1.  **初始位置**可以在每边 100 个单位的立方体中分布。

1.  **生命周期**可以是 4-5 秒。

1.  **初始速度**应该是从(-50,-50,-100)到(25,25,-10)之间的某个值。

1.  **初始颜色**可以是一个分布向量,其值为绿色、黄色和红色。

1.  **加速度**可以是(0,0,-20)。

1.  **初始旋转速率**可以是 0.25(最大值)。

1.  可以添加一个带有分布(0,0,0)到(0,10,10)的**轨道**参数。

1.  **风**:通过在**内容浏览器**的空白处右键单击,然后选择**新建材质参数集合**,创建一个**材质参数集合**(**MPC**)。

1.  双击编辑您的新材质参数集合,并输入一个新的参数`TheWind`。给它初始值`(1, 1, 1)`。

1.  在您的关卡蓝图(**蓝图** | **关卡蓝图**)中,创建一个名为`TheWind`的客户端变量。在事件`BeginPlay`中将`TheWind`变量初始化为`(1, 1, 1)`,然后在每帧将此变量发送到 GPU。

1.  在事件`Tick`中,根据自己的喜好修改风力。在我的版本中,我将每帧的风力乘以一个三维随机向量,其值在[-1,1]之间。这样可以使风力每帧都有一个不错的颤动效果。

1.  通过在修改风向量后立即选择一个**设置矢量参数值**节点,将风变量更新发送到 GPU。**设置矢量参数值**必须引用材质参数集合内的变量,因此引用在*步骤 4*中创建的材质参数集合内的`TheWind`变量。

1.  通过每帧修改`WorldPositionOffset`的`TheWind`的某个倍数。由于`TheWind`变量变化缓慢,每帧呈现的修改将是上一帧呈现的修改的轻微变化,从而产生平滑的叶子运动。![如何操作...](https://gitee.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/ue4-scp-cpp-cb/img/00281.jpeg)

## 它是如何工作的...

叶子以大致恒定的速率下落,但受到着色器内部不断变化的风向量的牵引。

# 反射率取决于观察角度

材质的反射率依赖于观察角度的倾向被称为**Fresnel**效应。材质在接近水平角度时可能比在正对角度时更具镜面反射性。

![反射率取决于观察角度](https://gitee.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/ue4-scp-cpp-cb/img/00282.jpeg)

### 注意

Fresnel 效果在接近水平角度时具有较大的幅度。由于使用了 Fresnel 效果,前面截图中的水材质在接近水平角度时具有较高的镜面反射和不透明度。

UE4 具有专门的内置功能来处理这个问题。我们将构建一个水材质,其中透明度具有视角依赖性,以便实际演示如何使用 Fresnel 效果。

## 准备工作

您需要一个要添加 Fresnel 效果的新材质。最好选择一个在观察角度不同的情况下看起来有些不同的材质。

## 如何操作...

1.  在材质内部,通过 Fresnel 节点的输出来驱动一个通道(不透明度、镜面反射或漫反射颜色)。

1.  Fresnel 节点的参数指数和基础反射分数可以调整如下:

1.  **指数**:描述材质的 Fresnel 程度。较高的值会夸大 Fresnel 效果。

1.  **基础反射分数**:较低的数值会夸大 Fresnel 效果。对于值为 1.0,Fresnel 效果不会显现。

## 它是如何工作的...

实现 Fresnel 效果背后有很多数学知识,但在材质中使用它来驱动组件相对较简单,并且可以帮助您创建一些非常漂亮的材质。

# 随机性 - 柏林噪声

一些着色器可以从使用随机值中受益。每个材质都有一些节点可以帮助给着色器添加随机性。可以使用**Perlin**噪声纹理的随机性来生成看起来有趣的材质,比如大理石材质。这种噪声还可以用于驱动凹凸贴图、高度贴图和位移场,产生一些炫酷的效果。

## 准备工作

选择一个你想要添加一些随机性的材质。在材质编辑器中打开该材质,并按照以下步骤进行操作。

## 如何操作...

1.  将一个**Noise**节点插入到你的材质编辑器窗口中。

1.  对你要添加噪声的对象的坐标进行归一化。你可以使用以下数学公式来实现:![如何操作...](https://gitee.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/ue4-scp-cpp-cb/img/00283.jpeg)

1.  从系统中的每个处理过的顶点中减去最小值,使对象位于原点。

1.  将顶点除以对象的大小,将对象放入一个单位盒子中。

1.  将顶点值乘以 2,将单位盒子从 1x1 扩展到 2x2。

1.  将顶点值减去 1,将单位移动到以原点为中心,值从*[-1,-1,-1]*到*[+1,+1,+1]*。

1.  选择一个值来绘制噪声。请记住,噪声在输入值在![如何操作...](https://gitee.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/ue4-scp-cpp-cb/img/00284.jpeg)之间时效果非常好。在这个范围之外,Perlin 噪声在缩小时会出现雪花状的外观(因为输出值在输入*x*上的变化太大)。

## 工作原理...

Perlin 噪声可以帮助你产生一些美丽的大理石纹理和图案。除了在图形中使用它,你还可以使用 Perlin 噪声以一种自然的方式驱动运动和其他现象。

# 给景观着色

构建景观着色器相对较容易。它们允许你为一个非常大的自定义几何体(称为景观)指定多重纹理。

## 准备工作

景观对象非常适合用作游戏世界级别的地面平面。你可以使用景观选项卡在同一级别中构建多个景观。通过点击**模式**面板中的山的图片,访问**景观**调色板,如下图所示:

![准备工作](https://gitee.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/ue4-scp-cpp-cb/img/00285.jpeg)

## 如何操作...

1.  通过点击**模式** | **景观**来构建一个新的景观对象。在**新景观**标题下,选择**创建新的**单选按钮。你将看到一个绿色的线框覆盖层,提供了新的景观。你可以使用**区块大小**和**每个组件的区块数**设置来调整其大小。

### 提示

当我们最终进行纹理贴图时,景观将以**区块大小** * **每个组件的区块数** * **组件的数量**的倍数平铺所选的纹理。如果你想让景观纹理平铺次数更少,可以记下这个数字,然后将馈送给纹理的 UV 坐标除以前一行计算出的数字。

1.  暂时不要点击对话框中的其他任何内容,因为我们还需要构建我们的景观材质。这在以下步骤中进行了概述。

1.  导航到**内容浏览器**,为你的景观创建一个新的材质。将其命名为`LandscapeMaterial`。

1.  通过双击编辑你的`LandscapeMaterial`。在空白处右键单击,选择一个`LandscapeCoordinate`节点,将 UV 坐标传递到我们即将应用的纹理中。

+   为了减少景观上的平铺效果,你需要将`LandscapeCoordinate`节点的输出除以景观的总大小(**区块大小** * **每个组件的区块数** * **组件的数量**)(如*步骤 1*中的提示所述)。

1.  在画布上添加一个`LandscapeLayerBlend`节点。将节点的输出导向**基本颜色**图层。

1.  点击`LandscapeLayerBlend`节点,在**详细信息**选项卡中为元素添加几个图层。这将允许你使用**纹理绘制**来在纹理之间进行混合。为每个图层命名,并从以下选项中选择混合方法:

+   通过绘制权重(LB 权重混合)。

+   通过纹理内的 alpha 值(LB Alpha 混合)。

+   按高度(LB 高度混合)。

1.  根据需要设置每个添加的`LandscapeLayer`的其他参数。

1.  为每个景观混合层提供纹理。

1.  通过将恒定的 0 输入添加到镜面输入中,将景观的高光减少到 0。

1.  保存并关闭您的材质。

1.  现在,转到**模式** | 景观选项卡,并在下拉菜单中选择您新创建的`LandscapeMaterial`。

1.  在**图层**部分,点击每个可用的景观图层旁边的**+**图标。为每个景观图层创建并保存一个目标图层对象。

1.  最后,向下滚动到景观选项卡,点击**创建**按钮。

1.  点击绘画选项卡,选择画笔大小和纹理,开始绘制景观纹理。

## 工作原理…

景观材质可以通过高度或手工艺进行混合,如本教程所示。


# 第十二章:使用 UE4 API

应用程序编程接口(API)是您作为程序员指示引擎和 PC 要执行的操作的方式。UE4 的所有功能都封装在模块中,包括非常基本和核心的功能。每个模块都有一个 API。要使用 API,有一个非常重要的链接步骤,在其中必须在`ProjectName.Build.cs`文件中列出您将在构建中使用的所有 API,该文件位于**Solution Explorer**窗口中。

### 提示

不要将任何 UE4 项目命名为与 UE4 API 名称完全相同的名称!

![使用 UE4 API](https://gitee.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/ue4-scp-cpp-cb/img/00286.jpeg)

UE4 引擎中有各种 API,可以向其各个重要部分公开功能。本章中我们将探索一些有趣的 API,包括以下内容:

+   Core/Logging API – 定义自定义日志类别

+   Core/Logging API – 使用`FMessageLog`将消息写入**Message Log**

+   Core/Math API – 使用`FRotator`进行旋转

+   Core/Math API – 使用`FQuat`进行旋转

+   Core/Math API – 使用`FRotationMatrix`进行旋转,使一个对象面向另一个对象

+   Landscape API – 使用 Perlin 噪声生成地形

+   Foliage API – 在您的关卡中以程序化方式添加树木

+   Landscape and Foliage APIs – 使用 Landscape 和 Foliage APIs 生成地图

+   GameplayAbilities API – 使用游戏控制触发角色的游戏能力

+   GameplayAbilities API – 使用`AttributeSet`实现统计数据

+   GameplayAbilities API – 使用`GameplayEffect`实现增益效果

+   GameplayTags API – 将`GameplayTags`附加到角色

+   GameplayTasks API – 使用`GameplayTasks`实现游戏任务

+   HTTP API – 网络请求

+   HTTP API – 进度条

# 介绍

UE4 引擎在编辑器中提供的基本功能非常广泛。C++代码的功能实际上被分组到称为 API 的小节中。UE4 代码库中的每个重要功能都有一个单独的 API 模块。这样做是为了保持代码库高度组织和模块化。

### 提示

使用不同的 API 可能需要在您的`Build.cs`文件中进行特殊链接!如果出现构建错误,请确保检查与正确的 API 的链接是否存在!

完整的 API 列表位于以下文档中:[`docs.unrealengine.com/latest/INT/API/`](https://docs.unrealengine.com/latest/INT/API/)。

# Core/Logging API – 定义自定义日志类别

UE4 本身定义了几个日志类别,包括`LogActor`等类别,其中包含与`Actor`类相关的任何日志消息,以及`LogAnimation`,用于记录有关动画的消息。一般来说,UE4 为每个模块定义了一个单独的日志类别。这允许开发人员将其日志消息输出到不同的日志流中。每个日志流的名称作为前缀添加到输出的消息中,如引擎中的以下示例日志消息所示:

```cpp
LogContentBrowser: Native class hierarchy updated for 'HierarchicalLODOutliner' in 0.0011 seconds. Added 1 classes and 2 folders.
LogLoad: Full Startup: 8.88 seconds (BP compile: 0.07 seconds)
LogStreaming:Warning: Failed to read file '../../../Engine/Content/Editor/Slate/Common/Selection_16x.png' error.
LogExternalProfiler: Found external profiler: VSPerf

以上是引擎中的示例日志消息,每个消息前都有其日志类别的前缀。警告消息以黄色显示,并在前面添加了Warning

您在互联网上找到的示例代码往往使用LogTemp作为 UE4 项目自己的消息,如下所示:

UE_LOG( LogTemp, Warning, TEXT( "Message %d" ), 1 );

我们实际上可以通过定义自己的自定义LogCategory来改进这个公式。

准备工作

准备一个 UE4 项目,您想要定义一个自定义日志。打开一个将在几乎所有使用此日志的文件中包含的头文件。

操作步骤...

  1. 打开您的项目的主头文件;例如,如果您的项目名称是Pong,则打开Pong.h。在#include Engine.h之后添加以下代码行:
DECLARE_LOG_CATEGORY_EXTERN( LogPong, Log, All ); // Pong.h

AssertionMacros.h中定义了此声明的三个参数,如下所示:

  • CategoryName:这是正在定义的日志类别名称(这里是LogPong

  • DefaultVerbosity:这是要在日志消息上使用的默认详细程度

  • CompileTimeVerbosity:这是编译代码中的详细程度

  1. 在项目的主.cpp文件中,包含以下代码行:
DEFINE_LOG_CATEGORY( LogPong ); // Pong.cpp
  1. 使用各种显示类别的日志,如下所示:
UE_LOG( LogPong, Display, TEXT( "A display message, log is working" ) ); // shows in gray
UE_LOG( LogPong, Warning, TEXT( "A warning message" ) );
UE_LOG( LogPong, Error, TEXT( "An error message " ) );

操作步骤

工作原理

日志通过将消息输出到“输出日志”(“窗口”|“开发者工具”|“输出日志”)以及文件中来工作。所有输出到“输出日志”的信息也会复制到项目的/Saved/Logs文件夹中的一个简单文本文件中。日志文件的扩展名为.log,其中最新的一个被命名为YourProjectName.log

还有更多...

您可以使用以下控制台命令在编辑器中启用或禁止特定日志通道的日志消息:

Log LogName off // Stop LogName from displaying at the output
Log LogName Log // Turn LogName's output on again

如果您想编辑一些内置日志类型的输出级别的初始值,可以使用 C++类来对Engine.ini配置文件进行更改。您可以在engine.ini配置文件中更改初始值。有关更多详细信息,请参见wiki.unrealengine.com/Logs,_Printing_Messages_To_Yourself_During_Runtime

另请参阅

  • UE_LOG将其输出发送到“输出窗口”。如果您还想使用更专门的“消息日志”窗口,您可以使用FMessageLog对象来编写输出消息。FMessageLog同时写入“消息日志”和“输出窗口”。有关详细信息,请参见下一个教程。

核心/日志 API - 使用 FMessageLog 将消息写入消息日志

FMessageLog是一个对象,允许您将输出消息同时写入“消息日志”(“窗口”|“开发者工具”|“消息日志”)和“输出日志”(“窗口”|“开发者工具”|“输出日志”)。

准备工作

准备好您的项目和一些要记录到“消息日志”的信息。在 UE4 编辑器中显示“消息日志”。以下屏幕截图是“消息日志”的样子:

准备就绪

操作步骤

  1. 在您的主头文件(ProjectName.h)中添加#define,将LOCTEXT_NAMESPACE定义为您的代码库中的某个唯一值:
#define LOCTEXT_NAMESPACE "Chapter12Namespace"

这个#defineLOCTEXT()宏使用,我们用它来生成FText对象,但在输出消息中看不到它。

  1. 通过在非常全局的位置构建您的FMessageLog来声明它。您可以在ProjectName.h文件中使用extern。考虑以下代码片段作为示例:
extern FName LoggerName;
extern FMessageLog Logger;
  1. 然后,在.cpp文件中定义并使用MessageLogModule注册您的FMessageLog。在构建时,请确保为您的记录器提供一个清晰且唯一的名称。它是您的日志类别将出现在“输出日志”中的日志消息左侧的位置。例如,ProjectName.cpp
#define FTEXT(x) LOCTEXT(x, x)
FName LoggerName( "Chapter12Log" );
FMessageLog CreateLog( FName name )
{
  FMessageLogModule& MessageLogModule = 
  FModuleManager::LoadModuleChecked<FMessageLogModule>
  ("MessageLog");
  FMessageLogInitializationOptions InitOptions;
  InitOptions.bShowPages = true;// Don't forget this!
  InitOptions.bShowFilters = true;
  FText LogListingName = FTEXT( "Chapter 12's Log Listing" );
  MessageLogModule.RegisterLogListing( LoggerName, LogListingName, InitOptions );
}
// Somewhere early in your program startup
// (eg in your GameMode constructor)
AChapter12GameMode::AChapter12GameMode()
{
  CreateLogger( LoggerName );
  // Retrieve the Log by using the LoggerName.
  FMessageLog logger( LoggerName );
  logger.Warning(
  FTEXT( "A warning message from gamemode ctor" ) );
}

提示

KEYLOCTEXT(第一个参数)必须是唯一的,否则您将得到一个先前散列的字符串。如果您愿意,您可以包含一个#define,将参数重复两次传递给LOCTEXT,就像我们之前做的那样。

#define FTEXT(x) LOCTEXT(x, x)
  1. 使用以下代码记录您的消息:
Logger.Info( FTEXT( "Info to log" ) );
Logger.Warning( FTEXT( "Warning text to log" ) );
Logger.Error( FTEXT( "Error text to log" ) );

此代码利用了之前定义的FTEXT()宏。请确保它在您的代码库中。

提示

在初始化后重新构建消息日志可以检索到原始消息日志的副本。例如,在代码的任何位置,您可以编写以下代码:

FMessageLog( LoggerName ).Info( FTEXT( "An info message" ) );

核心/数学 API - 使用 FRotator 进行旋转

在 UE4 中,旋转有着完整的实现,因此很难选择如何旋转您的对象。有三种主要方法——FRotatorFQuatFRotationMatrix。本教程概述了这三种不同方法之一——FRotator的构建和使用。使用这个方法和下面的两个教程,您可以一目了然地选择一个用于旋转对象的方法。

准备工作

有一个 UE4 项目,其中有一个你可以使用 C++接口的对象。例如,你可以构造一个从Actor派生的 C++类 Coin 来测试旋转。重写Coin::Tick()方法来应用你的旋转代码。或者,你可以在蓝图中的Tick事件中调用这些旋转函数。

在这个例子中,我们将以每秒一度的速度旋转一个物体。实际的旋转将是物体创建后累积的时间。为了获得这个值,我们只需调用GetWorld()->TimeSeconds

如何做到这一点...

  1. 创建一个名为Coin的自定义 C++派生类,继承自Actor类。

  2. 在 C++代码中,重写Coin派生类的::Tick()函数。这将允许你在每一帧中对角色进行更改。

  3. 构造你的FRotatorFRotators可以使用标准的俯仰、偏航和滚转构造函数来构造,如下例所示:

FRotator( float InPitch, float InYaw, float InRoll );
  1. 你的FRotator将按以下方式构造:
FRotator rotator( 0, GetWorld()->TimeSeconds, 0 );
  1. 在 UE4 中,对象的标准方向是前方朝下的+X轴。右侧是+Y轴,上方是+Z轴。如何做到这一点...

  2. 俯仰是绕Y轴(横向)旋转,偏航是绕Z轴(上下)旋转,滚转是绕X轴旋转。这在以下三点中最容易理解:

  • 俯仰:如果你想象一个 UE4 标准坐标系中的飞机,Y轴沿着翼展(俯仰将其向前和向后倾斜)

  • 偏航Z轴直上直下(偏航将其左右旋转)

  • 滚转X轴沿着飞机机身直线(滚转进行卷筒翻滚)

提示

你应该注意,在其他约定中,X轴是俯仰,Y轴是偏航,Z轴是滚转。

  1. 使用SetActorRotation成员函数将你的FRotator应用到你的角色上,如下所示:
FRotator rotator( 0, GetWorld()->TimeSeconds, 0 );
SetActorRotation( rotation );

核心/数学 API - 使用 FQuat 进行旋转

四元数听起来很吓人,但它们非常容易使用。你可能想通过以下视频来了解它们背后的理论数学:

然而,在这里我们不会涉及数学背景!实际上,你不需要对四元数的数学背景有太多的了解就能极其有效地使用它们。

准备工作

准备一个项目和一个具有重写::Tick()函数的Actor,我们可以在其中输入 C++代码。

如何做到这一点...

  1. 构造四元数时,最好使用以下构造函数:
FQuat( FVector Axis, float AngleRad );

注意

例如,定义一个扭曲旋转

四元数还定义了四元数加法、四元数减法、乘以标量和除以标量等运算,以及其他函数。它们非常有用,可以将物体以任意角度旋转,并将物体指向彼此。

它是如何工作的...

四元数有点奇怪,但使用它们非常简单。如果v是旋转的轴,它是如何工作的...是旋转角度的大小,那么我们可以得到以下四元数分量的方程:

它是如何工作的...

因此,例如,绕它是如何工作的...旋转它是如何工作的...角度将具有以下四元数分量:

它是如何工作的...

四元数的四个分量中的三个分量(xyz)定义了旋转的轴(乘以旋转角度一半的正弦值),而第四个分量(w)只有旋转角度一半的余弦值。

还有更多...

四元数本身是向量,可以进行旋转。只需提取四元数的(x, y, z)分量,进行归一化,然后旋转该向量。使用所需旋转角度构造一个新的四元数,该四元数由该新单位向量构成。

将四元数相乘表示一系列连续发生的旋转。例如,绕X轴旋转 45º,然后绕Y轴旋转 45º将由以下组成:

FQuat( FVector( 1, 0, 0 ), PI/4.f ) *
FQuat( FVector( 0, 1, 0 ), PI/4.f );

核心/数学 API-使用 FRotationMatrix 进行旋转,使一个对象面向另一个对象

FRotationMatrix提供了使用一系列::Make*例程进行矩阵构造的功能。它们易于使用,对于使一个对象面向另一个对象非常有用。假设您有两个对象,其中一个对象跟随另一个对象。我们希望跟随者的旋转始终面向其所跟随的对象。FRotationMatrix的构造方法使这一点变得容易。

准备好了

在场景中有两个演员,其中一个应该面向另一个演员。

如何做到这一点...

  1. 在跟随者的Tick()方法中,查看FRotationMatrix类下可用的构造函数。提供了一系列构造函数,可以通过重新定位一个或多个XYZ轴来指定对象的旋转,命名为FRotationMatrix::Make*()模式。

  2. 假设您的演员具有默认的初始方向(前进沿着+X轴向下,向上沿着+Z轴向上),请找到从跟随者到他所跟随的对象的向量,如下所示:

FVector toFollow = target->GetActorLocation() - GetActorLocation();
FMatrix rotationMatrix = FRotationMatrix::MakeFromXZ( toTarget, GetActorUpVector() );
SetActorRotation( rotationMatrix.Rotator() );

它是如何工作的...

使一个对象看向另一个对象,并具有所需的上向量,可以通过调用正确的函数来完成,具体取决于对象的默认方向。通常,您希望重新定位X轴(前进),同时指定Y轴(右)或Z轴(上)向量(FRotationMatrix::MakeFromXY())。例如,要使一个演员沿着lookAlong向量朝向,其右侧面向右侧,我们可以构造并设置其FRotationMatrix如下:

FRotationMatrix rotationMatrix = FRotationMatrix::MakeFromXY( lookAlong, right );
actor->SetActorRotation( rotationMatrix.Rotator() );

景观 API-使用 Perlin 噪声生成景观

如果您在场景中使用ALandscape,您可能希望使用代码而不是手动刷入来编程设置其高度。要在代码中访问ALandscape对象及其函数,您必须编译和链接LandscapeLandscapeEditorAPI。

景观 API-使用 Perlin 噪声生成景观

准备好了

生成景观并不是非常具有挑战性。您需要链接LandscapeLandscapeEditorAPI,并且还需要以编程方式设置地图上的高度值。在本示例中,我们将展示如何使用 Perlin 噪声来实现这一点。

以前,您可能已经看到过 Perlin 噪声用于着色,但这并不是它的全部用途。它也非常适用于地形高度。您可以将多个 Perlin 噪声值相加,以获得美丽的分形噪声。值得简要研究 Perlin 噪声,以了解如何获得良好的输出。

如何做到这一点...

  1. webstaff.itn.liu.se/~stegu/aqsis/aqsis-newnoise/检索 Perlin 噪声模块。您需要的两个文件是noise1234.hnoise1234.cpp(或者您可以从此存储库中选择另一对噪声生成文件)。将这些文件链接到您的项目中,并确保在noise1234.cpp#include YourPrecompiledHeader.h

  2. 在您的Project.Build.cs文件中链接LandscapeLandscapeEditorAPI。

  3. 使用 UMG 构建一个界面,允许您点击一个生成按钮来调用一个 C++函数,最终用 Perlin 噪声值填充当前景观。您可以按照以下步骤进行操作:

  • 右键单击内容浏览器,选择用户界面 | 小部件蓝图

  • 使用一个单独的按钮填充Widget Blueprint,该按钮启动一个单独的Gen()函数。Gen()函数可以附加到你的Chapter12GameMode派生类对象上,因为从引擎中检索它很容易。Gen()函数必须是BlueprintCallable UFUNCTION()。(有关如何执行此操作的详细信息,请参见第二章中的创建 UFUNCTION部分,创建类。)操作步骤…

  • 确保通过在其中一个启动蓝图中创建并将其添加到视口来显示你的 UI;例如,在你的 HUD 的BeginPlay事件中。

操作步骤…

  1. 使用 UE4 编辑器创建一个景观。假设景观将保持在屏幕上。我们只会使用代码修改它的值。

  2. 在你的地图生成例程中,使用以下代码修改你的ALandscape对象:

  • 通过搜索Level中的所有对象来找到级别中的Landscape对象。我们使用一个返回级别中所有Landscape实例的TArray的 C++函数来实现这一点:
TArray<ALandscape*> AChapter12GameMode::GetLandscapes()
{
  TArray<ALandscape*> landscapes;
  ULevel *level = GetLevel();
  for( int i = 0; i < level->Actors.Num(); i++ )
  if( ALandscape* land = Cast<ALandscape>(level->Actors[i]) )
  landscapes.Push( land );
  return landscapes;
}
  • 使用以下非常重要的行初始化世界的ULandscapeInfo对象,如下所示:
ULandscapeInfo::RecreateLandscapeInfo( GetWorld(), 1 );

注意

上一行代码非常重要。如果没有它,ULandscapeInfo对象将不会被初始化,你的代码将无法工作。令人惊讶的是,这是ULandscapeInfo类的静态成员函数,因此它会初始化级别中的所有ULandscapeInfo对象。

  • 获取你的ALandscape对象的范围,以便我们可以计算需要生成的高度值的数量。

  • 创建一组高度值来替换原始值。

  • 调用LandscapeEditorUtils::SetHeightmapData( landscape, data );将新的地形高度值放入你的ALandscape对象中。

例如,使用以下代码:

// a) REQUIRED STEP: Call static function
// ULandscapeInfo::RecreateLandscapeInfo().
// What this does is populate the Landscape object with
// data values so you don't get nulls for your 
// ULandscapeInfo objects on retrieval.
ULandscapeInfo::RecreateLandscapeInfo( GetWorld(), 1 );

// b) Assuming landscape is your landscape object pointer,
// get extents of landscape, to compute # height values
FIntRect landscapeBounds = landscape->GetBoundingRect();

// c) Create height values.
// LandscapeEditorUtils::SetHeightmapData() adds one to 
// each dimension because the boundary edges may be used.
int32 numHeights = (rect.Width()+1)*(rect.Height()+1);
TArray<uint16> Data;
Data.Init( 0, numHeights );
for( int i = 0; i < Data.Num(); i++ ) {
  float nx = (i % cols) / cols; // normalized x value
  float ny = (i / cols) / rows; // normalized y value
  Data[i] = PerlinNoise2D( nx, ny, 16, 4, 4 );
}

// d) Set values in with call:
LandscapeEditorUtils::SetHeightmapData( landscape, Data );

提示

当地图完全平坦时,heightmap的初始值将全部为32768SHRT_MAX(或USHRT_MAX/2+1))。这是因为地图使用无符号短整数(uint16)作为其值,使其无法取负值。为了使地图低于z=0,程序员将默认值设为heightmap的最大值的一半。

它是如何工作的…

Perlin 噪声函数用于为(xy)坐标对生成高度值。使用 2D 版本的 Perlin 噪声,以便我们可以根据 2D 空间坐标获取 Perlin 噪声值。

还有更多内容…

你可以使用地图的空间坐标来玩弄 Perlin 噪声函数,并将地图的高度分配给 Perlin 噪声函数的不同组合。你将希望使用多个 Octave 的 Perlin 噪声函数的总和来获得更多的地形细节。

PerlinNoise2D生成函数如下所示:

uint16 AChapter12GameMode::PerlinNoise2D( float x, float y,
  float amp, int32 octaves, int32 px, int32 py )
{
  float noise = 0.f;
  for( int octave = 1; octave < octaves; octave *= 2 )
  {
    // Add in fractions of faster varying noise at lower 
    // amplitudes for higher octaves. Assuming x is normalized, 
    // WHEN octave==px  you get full period. Higher frequencies 
    // will go out and also meet period.
    noise += Noise1234::pnoise( x*px*octave, y*py*octave, px, py ) / octave;
  }
  return USHRT_MAX/2.f + amp*noise;
}

PerlinNoise2D函数考虑到函数的中间值(海平面或平地)应该具有SHRT_MAX32768)的值。

Foliage API - 使用代码将树木程序化地添加到你的级别中

Foliage API 是使用代码填充级别中的树木的好方法。如果你这样做,那么你可以获得一些不错的结果,而不必手动产生自然的随机性。

我们将根据 Perlin 噪声值与植被的放置位置相关联,以便在 Perlin 噪声值较高时在给定位置放置树木的机会更大。

准备工作

在使用 Foliage API 的代码接口之前,你应该尝试使用编辑器中的功能来熟悉该功能。之后,我们将讨论使用代码接口在级别中放置植被。

提示

重要!请记住,FoliageType对象的材质必须在其面板中选中Used with Instanced Static Meshes复选框。如果不这样做,那么该材质将无法用于着色植被材质。

准备工作

确保为您在FoliageType上使用的材质勾选与实例化静态网格一起使用复选框,否则您的植被将显示为灰色。

操作步骤如下:

手动

  1. 模式面板中,选择带有叶子的小型植物的图片手动

  2. 单击+ 添加植被类型下拉菜单,并选择构建一个新的Foliage对象。

  3. 按您希望的名称保存Foliage对象。

  4. 双击以编辑您的新Foliage对象。从项目中选择网格,最好是树形状的对象,以在景观中绘制植被。

  5. 调整画笔大小和绘画密度以适合您的喜好。左键单击开始在植被中绘画。

  6. Shift + 单击以擦除您放置的植被。擦除密度值告诉您在擦除时要留下多少植被。

程序化

如果您希望引擎为您在关卡中分布植被,您需要完成以下几个步骤:

  1. 转到内容浏览器,右键单击创建一些FoliageType对象以在关卡中进行程序化分布。

  2. 点击编辑 | 编辑器首选项

  3. 点击实验选项卡。

  4. 启用程序化植被复选框。这允许您从编辑器中访问程序化植被类。

  5. 返回内容浏览器,右键单击并创建杂项 | 程序化植被生成器

  6. 双击打开您的程序化植被生成器,并选择在步骤 1 中创建的FoliageTypes

  7. 将您的程序化植被生成器拖放到关卡中,并调整大小,使其包含您想要布置程序化植被的区域。

  8. 从画笔菜单中,拖动几个程序化植被阻挡体积。将其中几个放置在程序化植被生成器体积内,以阻止植被出现在这些区域。

  9. 向下打开菜单,点击模拟程序化植被生成器应该会填充植被。

  10. 尝试不同的设置以获得您喜欢的植被分布。

另请参阅

  • 前面的示例在游戏开始前生成植被。如果您对运行时程序化植被生成感兴趣,请参阅下一个示例,Landscape and Foliage API - 使用 Landscape 和 Foliage API 进行地图生成

Landscape and Foliage API - 使用 Landscape 和 Foliage API 进行地图生成

我们可以使用前面提到的地形生成代码创建一个地形,并使用程序化植被功能在其上随机分布一些植被。

结合 Landscape API 和 Foliage API 的功能,您可以程序化生成完整的地图。在本示例中,我们将概述如何实现这一点。

我们将使用代码编程创建一个地形,并使用代码填充植被。

Landscape and Foliage API - 使用 Landscape 和 Foliage API 进行地图生成

准备工作

为了准备执行此示例,我们需要一个 UE4 项目,其中包含一个Generate按钮来启动生成。您可以参考Landscape API - 使用 Perlin 噪声生成地形示例来了解如何做到这一点。您只需要创建一个小的 UMG UI 小部件,其中包含一个Generate按钮。将您的Generate按钮的OnClick事件连接到 C++全局对象中的一个 C++ UFUNCTION(),例如您的Chapter12GameMode对象,该对象将用于生成地形。

操作步骤如下:

  1. 进入一个循环,尝试放置N棵树,其中N是要随机放置的树木数量,由Chapter12GameMode对象的UPROPERTY()指定。

  2. 从包围地形对象的 2D 框中获取随机 XY 坐标。

  3. 获取 Perlin 噪声值@(x, y)。您可以使用与用于确定植被放置的地形高度的 Perlin 噪声公式不同的 Perlin 噪声公式。

  4. 生成一个随机数。如果生成的数字在 Perlin 噪声函数的单位范围内,则使用SpawnFoliageInstance函数放置一棵树。否则,不要在那里放置一棵树。

提示

您应该注意到,我们使用所选择的位置的底层随机性来覆盖位置的随机性。在那里放置一棵树的实际机会取决于那里的 Perlin 噪声值,以及它是否在PerlinTreeValue的单位范围内。

非常密集的树分布将看起来像地图上的等值线。等值线的宽度是单位的范围。

它是如何工作的...

Perlin 噪声通过生成平滑的噪声来工作。对于区间中的每个位置(比如[-1, 1]),都有一个平滑变化的 Perlin 噪声值。

它是如何工作的...

Perlin 噪声值在 2D 纹理上进行采样。在每个像素(甚至在像素之间),我们可以得到一个非常平滑变化的噪声值。

在跨越 Perlin 噪声函数的距离上添加八度(或整数倍)到某个变量中,可以得到锯齿状的效果;例如,云朵中的小丛和山脉中的岩壁是通过更宽间隔的样本获得的,这些样本给出了更快变化的噪声。

为了获得漂亮的 Perlin 噪声输出,我们只需对采样的 Perlin 噪声值应用数学函数;例如,sin 和 cos 函数可以为您生成一些很酷的大理石效果。

提示

通过此处链接的实现提供的 Perlin 噪声函数,Perlin 噪声变得周期性,即可平铺。默认情况下,Perlin 噪声不是周期性的。如果您需要 Perlin 噪声是周期性的,请注意调用哪个库函数。

基本的 Perlin 噪声函数是一个确定性函数,每次调用它时都会返回相同的值。

还有更多...

您还可以在Chapter12GameMode对象派生类中设置滑块,以影响植被和地形的生成,包括以下参数:

  • 地形的振幅

  • 植被密度

  • 植被的等值线水平

  • 植被高度或比例的方差

GameplayAbilities API - 使用游戏控制触发角色的游戏能力

GameplayAbilities API 可用于将 C++函数附加到特定按钮推送上,在游戏单位在游戏中对按键事件的响应中展示其能力。在本教程中,我们将向您展示如何做到这一点。

准备工作

枚举并描述游戏角色的能力。您需要知道您的角色对按键事件的响应以编码此处的代码。

这里有几个我们需要使用的对象,它们如下:

  • UGameplayAbility类 - 这是为了派生 C++类的UGameplayAbility类实例,每个能力都有一个派生类。

  • 通过重写可用函数(如UGameplayAbility::ActivateAbilityUGameplayAbility::InputPressedUGameplayAbility::CheckCostUGameplayAbility::ApplyCostUGameplayAbility::ApplyCooldown等)在.h.cpp中定义每个能力的功能。

  • GameplayAbilitiesSet - 这是一个DataAsset派生对象,包含一系列枚举的命令值,以及定义该特定输入命令行为的UGameplayAbility派生类的蓝图。每个 GameplayAbility 都由按键或鼠标点击触发,这在DefaultInput.ini中设置。

操作步骤...

在接下来的内容中,我们将为Warrior类对象实现一个名为UGameplayAbility_AttackUGameplayAbility派生类。我们将把这个游戏功能附加到输入命令字符串Ability1上,然后在鼠标左键点击时激活它。

  1. ProjectName.Build.cs文件中链接GameplayAbilities API。

  2. UGameplayAbility派生一个 C++类。例如,编写一个 C++ UCLASS UGameplayAbility_Attack

  3. 至少,您需要重写以下内容:

  • 使用UGameplayAbility_Attack::CanActivateAbility成员函数来指示角色何时可以调用该能力。

  • 使用UGameplayAbility_Attack::CheckCost函数来指示玩家是否能够负担得起使用能力。这非常重要,因为如果返回 false,能力调用应该失败。

  • 使用UGameplayAbility_Attack::ActivateAbility成员函数,并编写当Warrior激活他的Attack能力时要执行的代码。

  • 使用UGameplayAbility_Attack::InputPressed成员函数,并响应分配给该能力的按键输入事件。

  1. 在 UE4 编辑器中从您的UGameplayAbility_Attack对象派生一个蓝图类。

  2. 在编辑器中,导航到内容浏览器并创建一个GameplayAbilitiesSet对象:

  • 右键单击内容浏览器,选择杂项 | 数据资产

  • 在随后的对话框中,选择GameplayAbilitySet作为数据资产类

提示

实际上,GameplayAbilitySet对象是一个UDataAsset派生类。它位于GameplayAbilitySet.h中,并包含一个单一的成员函数GameplayAbilitySet::GiveAbilities(),我强烈建议您不要使用,原因将在后面的步骤中列出。

如何操作...

  1. 将您的GameplayAbilitySet数据资产命名为与Warrior对象相关的名称,以便我们知道要将其选择到Warrior类中(例如,WarriorGameplayAbilitySet)。

  2. 双击打开并编辑新的WarriorAbilitySet数据资产。通过在其中的TArray对象上点击+,将一系列GameplayAbility类派生蓝图堆叠在其中。您的UGameplayAbility_Attack对象必须出现在下拉列表中。

  3. UPROPERTY UGameplayAbilitySet* gameplayAbilitySet成员添加到您的Warrior类中。编译、运行,并在内容浏览器中选择WarriorAbilitySet(在步骤 5 到 7 中创建)作为此Warrior能够使用的能力。

  4. 确保您的Actor类派生类也派生自UAbilitySystemInterface接口。这非常重要,以便对(Cast<IAbilitySystemInterface>(yourActor))->GetAbilitySystemComponent()的调用成功。

  5. 在构建角色之后的某个时候,调用gameplayAbilitySet->GiveAbilities(abilitySystemComponent);或进入一个循环,如下一步所示,在其中为您的gameplayAbilitySet中列出的每个能力调用abilitySystemComponent->GiveAbility()

  6. AWarrior::SetupPlayerInputComponent(UInputComponent* Input)编写一个重写,将输入控制器连接到 Warrior 的 GameplayAbility 激活。这样做后,迭代每个在您的 GameplayAbilitySet 的Abilities组中列出的 GameplayAbility。

提示

不要使用GameplayAbilitySet::GiveAbilities()成员函数,因为它不会给您访问实际上需要绑定和调用能力到输入组件的一组FGameplayAbilitySpecHandle对象。

void AWarrior::SetupPlayerInputComponent( UInputComponent* Input )
{
  Super::SetupPlayerInputComponent( Input );
  // Connect the class's AbilitySystemComponent
  // to the actor's input component
  AbilitySystemComponent->BindToInputComponent( Input );

  // Go thru each BindInfo in the gameplayAbilitySet.
  // Give & try and activate each on the AbilitySystemComponent.
  for( const FGameplayAbilityBindInfo& BindInfo : 
  gameplayAbilitySet->Abilities )
  {
    // BindInfo has 2 members:
    //   .Command (enum value)
    //   .GameplayAbilityClass (UClass of a UGameplayAbility)
    if( !BindInfo.GameplayAbilityClass )
    {
      Error( FS( "GameplayAbilityClass %d not set",
      (int32)BindInfo.Command ) );
      continue;
    }

    FGameplayAbilitySpec spec(
    // Gets you an instance of the UClass
    BindInfo.GameplayAbilityClass->
    GetDefaultObject<UGameplayAbility>(),
    1, (int32)BindInfo.Command ) ;

 // STORE THE ABILITY HANDLE FOR LATER INVOKATION
 // OF THE ABILITY
    FGameplayAbilitySpecHandle abilityHandle = 
    AbilitySystemComponent->GiveAbility( spec );

    // The integer id that invokes the ability 
    // (ith value in enum listing)
    int32 AbilityID = (int32)BindInfo.Command;

    // CONSTRUCT the inputBinds object, which will
    // allow us to wire-up an input event to the
    // InputPressed() / InputReleased() events of
    // the GameplayAbility.
    FGameplayAbiliyInputBinds inputBinds(
      // These are supposed to be unique strings that define
      // what kicks off the ability for the actor instance.
      // Using strings of the format 
      // "ConfirmTargetting_Player0_AbilityClass"
      FS( "ConfirmTargetting_%s_%s", *GetName(), 
        *BindInfo.GameplayAbilityClass->GetName() ),
      FS( "CancelTargetting_%s_%s", *GetName(), 
        *BindInfo.GameplayAbilityClass->GetName() ),
      "EGameplayAbilityInputBinds", // The name of the ENUM that 
      // has the abilities listing (GameplayAbilitySet.h).
      AbilityID, AbilityID
    );
 // MUST BIND EACH ABILITY TO THE INPUTCOMPONENT, OTHERWISE
 // THE ABILITY CANNOT "HEAR" INPUT EVENTS.
    // Enables triggering of InputPressed() / InputReleased() 
    // events, which you can in-turn use to call 
    // TryActivateAbility() if you so choose.
    AbilitySystemComponent->BindAbilityActivationToInputComponent(
      Input, inputBinds
    );

    // Test-kicks the ability to active state.
    // You can try invoking this manually via your
    // own hookups to keypresses in this Warrior class
    // TryActivateAbility() calls ActivateAbility() if
    // the ability is indeed invokable at this time according
    // to rules internal to the Ability's class (such as cooldown
    // is ready and cost is met)
    AbilitySystemComponent->TryActivateAbility( 
      abilityHandle, 1 );
  }
}

它是如何工作的...

您必须通过一系列对UAbilitySystemComponent::GiveAbility(spec)的调用,将一组UGameplayAbility对象子类化并链接到您的角色的UAbilitySystemComponent对象中,其中包括适当构造的FGameplayAbilitySpec对象。这样做的目的是为您的角色装备这一组GameplayAbilities。每个UGameplayAbility的功能、成本、冷却和激活都被整洁地包含在您将构建的UGameplayAbility类派生类中。

还有更多...

您将需要仔细编写一堆其他可在GameplayAbility.h头文件中使用的函数,包括以下实现:

  • SendGameplayEvent:这是一个通知 GameplayAbility 发生了一些常规游戏事件的函数。

  • CancelAbility:这是一个函数,用于在使用能力过程中停止能力,并给予能力中断状态。

  • 请记住,在UGameplayAbility类声明的底部附近有一堆现有的UPROPERTY,它们在添加或删除某些GameplayTags时激活或取消能力。有关详细信息,请参阅以下GameplayTags API - 将 GameplayTags 附加到 Actor的示例。

  • 还有更多!探索 API 并在代码中实现那些您认为有用的功能。

另请参阅

  • GameplayAbilities API 是一系列丰富且巧妙交织的对象和函数。真正探索GameplayEffectsGameplayTagsGameplayTasks以及它们如何与UGameplayAbility类集成,以充分探索库所提供的功能。

GameplayAbilities API - 使用 UAttributeSet 实现统计信息

GameplayAbilities API 允许您将一组属性(即UAttributeSet)与 Actor 关联起来。UAttributeSet描述了适用于该 Actor 的游戏属性的属性,例如HpManaSpeedArmorAttackDamage等等。您可以定义一个适用于所有 Actor 的单个全局游戏属性集,或者适用于不同类别的 Actor 的几个不同的属性集。

准备就绪

AbilitySystemComponent是您需要添加到 Actor 中的第一件事,以使其能够使用GameAbilities APIUAttributeSet。要定义自定义的UAttributeSet,您只需从UAttributeSet基类派生,并使用自己的一系列UPROPERTY成员扩展基类。之后,您必须将自定义的AttributeSet注册到Actor类的AbilitySystemComponent中。

如何做...

  1. ProjectName.Build.cs文件中链接到GameplayAbilities API。

  2. 在自己的文件中,从UAttributeSet类派生,并使用一组UPROPERTY装饰该类,这些属性将在每个 Actor 的属性集中使用。例如,您可能希望声明类似于以下代码片段的UAttributeSet派生类:

#include "Runtime/GameplayAbilities/Public/AttributeSet.h"
#include "GameUnitAttributeSet.generated.h"

UCLASS(Blueprintable, BlueprintType)
class CHAPTER12_API UGameUnitAttributeSet : public UAttributeSet
{
  GENERATED_BODY()
  public:
  UGameUnitAttributeSet( const FObjectInitializer& PCIP );
  UPROPERTY( EditAnywhere, BlueprintReadWrite, Category = GameUnitAttributes )  float Hp;
  UPROPERTY( EditAnywhere, BlueprintReadWrite, Category = GameUnitAttributes )  float Mana;
  UPROPERTY( EditAnywhere, BlueprintReadWrite, Category = GameUnitAttributes )  float Speed;
};

提示

如果您的代码是网络化的,您可能希望在UPROPERTY的每个副本声明中启用复制。

  1. 通过调用以下代码将GameUnitAttributeSetActor类中的AbilitySystemComponent连接起来:
AbilitySystemComponent->InitStats( 
  UGameUnitAttributeSet::StaticClass(), NULL );

您可以将此调用放在PostInitializeComponents()的某个位置,或者在稍后调用的代码中。

  1. 一旦您注册了UAttributeSet,您可以继续下一个步骤,并将GameplayEffect应用于属性集中的某些元素。

  2. 确保您的Actor类对象通过从其派生来实现IAbilitySystemInterface。这非常重要,因为UAbilitySet对象将尝试将其转换为IAbilitySystemInterface,以在代码的各个位置调用GetAbilitySystemComponent()

工作原理...

UAttributeSets只是允许您枚举和定义不同 Actor 的属性。GameplayEffects将是您对特定 Actor 的属性进行更改的手段。

还有更多...

您可以编写GameplayEffects的定义,这些定义将对 AbilitySystemComponent 的AttributeSet集合产生影响。您还可以编写GameplayTasks,用于在特定时间或事件运行的通用函数,甚至是响应标签添加(GameplayTagResponseTable.cpp)。您可以定义GameplayTags来修改 GameplayAbility 的行为,并在游戏过程中选择和匹配游戏单位。

GameplayAbilities API - 使用 GameplayEffect 实现增益效果

A buff is just an effect that introduces a temporary, permanent, or recurring change to a game unit's attributes from its AttributeSet. Buffs can either be good or bad, supplying either bonuses or penalties. For example, you might have a hex buff that slows a unit to half speed, an angel wing buff that increases unit speed by 2x, or a cherub buff that recovers 5 hp every five seconds for three minutes. A GameplayEffect affects an individual gameplay attributes in the UAttributeSet attached to an AbilitySystemComponent of an Actor.

Getting ready

Brainstorm your game units' effects that happen during the game. Be sure that you've created an AttributeSet, shown in the previous recipe, with gameplay attributes that you'd like to affect. Select an effect to implement and follow the succeeding steps with your example.

Tip

You may want to turn LogAbilitySystem to a VeryVerbose setting by going to the Output Log and typing ```cpp, and then Log LogAbilitySystem All.

This will display much more information from AbilitySystem in the Output Log, making it easier to see what's going on within the system.

How to do it…

In the following steps, we'll construct a quick GameplayEffect that heals 50 hp to the selected unit's AttributeSet:

  1. Construct your UGameplayEffect class object using the CONSTRUCT_CLASS macro with the following line of code:

    
    

// Create GameplayEffect recovering 50 hp one time only to unit

CONSTRUCT_CLASS( UGameplayEffect, RecoverHP );


2.  Use the `AddModifier` function to change the `Hp` field of `GameUnitAttributeSet`, as follows:

AddModifier( RecoverHP,

GET_FIELD_CHECKED( UGameUnitAttributeSet, Hp ),

EGameplayModOp::Additive, FScalableFloat( 50.f ) );


3.  Fill in the other properties of `GameplayEffect`, including fields such as `DurationPolicy` and `ChanceToApplyToTarget` or any other fields that you'd like to modify, as follows:

RecoverHP->DurationPolicy = EGameplayEffectDurationType::HasDuration;

RecoverHP->DurationMagnitude = FScalableFloat( 10.f );

RecoverHP->ChanceToApplyToTarget = 1.f;

RecoverHP->Period = .5f;


4.  Apply the effect to an `AbilitySystemComponent` of your choice. The underlying `UAttributeSet` will be affected and modified by your call, as shown in the following piece of code:

FActiveGameplayEffectHandle recoverHpEffectHandle =

AbilitySystemComponent->ApplyGameplayEffectToTarget( RecoverHP,

AbilitySystemComponent, 1.f );


## How it works…

`GameplayEffects` are simply little objects that effect changes to an actor's `AttributeSet`. `GameplayEffects` can occur once, or repeatedly, in intervals over a `Period`. You can program-in effects pretty quickly and the `GameplayEffect` class creation is intended to be inline.

## There's more…

Once the `GameplayEffect` is active, you will receive an `FActiveGameplayEffectHandle`. You can use this handle to attach a function delegate to run when the effect is over using the `OnRemovedDelegate` member of the `FActiveGameplayEffectHandle`. For example, you might call:

FActiveGameplayEffectHandle recoverHpEffectHandle =

AbilitySystemComponent->ApplyGameplayEffectToTarget( RecoverHP,

AbilitySystemComponent, 1.f );

if( recoverHpEffectHandle ) {

recoverHpEffectHandle->AddLambda( {

Info( "RecoverHp Effect has been removed." );

} );

}


# GameplayTags API – Attaching GameplayTags to an Actor

`GameplayTags` are just small bits of text that describes states (or buffs) for the player or attributes that can attach to things such as `GameplayAbilities` and also to describe `GameplayEffects`, as well as states that clear those effects. So, we can have `GameplayTags`, such as `Healing` or `Stimmed`, that trigger various `GameplayAbilities` or `GameplayEffects` to our liking. We can also search for things via `GameplayTags` and attach them to our `AbilitySystemComponents` if we choose.

## How to do it…

There are several steps to getting `GameplayTags` to work correctly inside your engine build; they are as follows:

1.  First, we will need to create a Data Table asset to carry all of our game's tag names. Right-click on **Content Browser** and select **Miscellaneous** | **Data Table**. Select a table class structure deriving from `GameplayTagTableRow`.![How to do it…](https://gitee.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/ue4-scp-cpp-cb/img/00303.jpeg)

List all tags available inside your game under that data structure.

2.  Add `UPROPERTY() TArray<FString>` to your `GameMode` object to list the names of the `TagTableNames` that you want to load into the `GameplayTags` module manager:

UPROPERTY( EditAnywhere, BlueprintReadWrite, Category = GameplayTags )

TArray GameplayTagTableNames;


3.  In your GameMode's `PostInitializeComponents` function, or later, load the tags in the tables of your choice using `GetGameplayTagsManager`:

IGameplayTagsModule::Get().GetGameplayTagsManager().

LoadGameplayTagTable( GameplayTagTableNames );


4.  Use your `GameplayTags`. Inside each of your GameplayAbility objects, you can modify the blockedness, cancelability, and activation requirements for each GameplayAbility using tag attachment or removal.![How to do it…](https://gitee.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/ue4-scp-cpp-cb/img/00304.jpeg)

You do have to rebuild your engine in order to get your tags to load within the editor. The patch to the engine source that is proposed allows you to hook in a call to `IGameplayTagsModule::Get().GetGameplayTagsManager().LoadGameplayTagTable( GameplayTagTableNames )`.

To get this call embedded into the editor's startup, you will need to edit the engine's source.

# GameplayTasks API – Making things happen with GameplayTasks

`GameplayTasks` are used to wrap up some gameplay functionality in a reusable object. All you have to do to use them is derive from the `UGameplayTask` base class and override some of the member functions that you prefer to implement.

## Getting ready

Go in the UE4 Editor and navigate to **Class Viewer**. Ensure that you have linked in the `GameplayTasks` API into your `ProjectName.Build.cs` file and search with **Actors Only** tickbox off for the `GameplayTask` object type.

## How to do it…

1.  Ensure that you have linked `GameplayTasks` API into your `ProjectName.Build.cs` file.
2.  Click on **File** | **Add C++ Class…** Choose to derive from `GameplayTask`. To do so, you must first tick **Show All Classes**, and then type `gameplaytask` into the filter box. Click on **Next**, name your C++ class (something like `GameplayTask_TaskName` is the convention) then add the class to your project. The example spawns a particle emitter and is called `GameplayTask_CreateParticles`.
3.  Once your `GameplayTask_CreateParticles.h` and `.cpp` pair are created, navigate to the `.h` file and declare a static constructor that creates a `GameplayTask_CreateParticles` object for you:

// Like a constructor.

UGameplayTask_CreateParticles* UGameplayTask_CreateParticles::ConstructTask(

TScriptInterface TaskOwner,

UParticleSystem* particleSystem,

FVector location )

{

UGameplayTask_CreateParticles* task =

NewTask<UGameplayTask_CreateParticles>( TaskOwner );

// Fill fields

if( task )

{

task->ParticleSystem = particleSystem;

task->Location = location;

}

return task;

}


4.  Override the `UGameplayTask_CreateEmitter::Activate()` function, which contains code that runs when `GameplayTask` is effected, as follows:

void UGameplayTask_CreateEmitter::Activate()

{

Super::Activate();

UGameplayStatics::SpawnEmitterAtLocation( GetWorld(),

ParticleSystem->GetDefaultObject(),

Location );

}


5.  Add `GameplayTasksComponent` to your `Actor` class derivative, which is available in the **Components** dropdown of the **Components** tab in the Blueprint editor.
6.  Create and add an instance of your `GameplayTask` inside your `Actor` derivative instance using the following code:

UGameplayTask_CreateParticles* task =

UGameplayTask_CreateParticles::ConstructTask( this,

particleSystem, FVector( 0.f, 0.f, 200.f ) );

if( GameplayTasksComponent )

{

GameplayTasksComponent->AddTaskReadyForActivation( *task );

}


7.  This code runs anywhere in your `Actor` class derivative, any time after `GameplayTasksComponent` is initialized (any time after `PostInitializeComponents()`).

## How it works…

`GameplayTasks` simply register with the `GameplayTasksComponent` situated inside an `Actor` class derivative of your choice. You can activate any number of `GameplayTasks` at any time during gameplay to trigger their effects.

`GameplayTasks` can also kick off `GameplayEffects` to change attributes of `AbilitySystemsComponents` if you wish.

## There's more…

You can derive `GameplayTasks` for any number of events in your game. What's more is that you can override a few more virtual functions to hook into additional functionality.

# HTTP API – Web request

When you're maintaining scoreboards or other such things that require regular HTTP request access to servers, you can use the HTTP API to perform such web request tasks.

## Getting ready

Have a server to which you're allowed to request data via HTTP. You can use a public server of any type to try out HTTP requests if you'd like.

## How to do it…

1.  Link to the HTTP API in your `ProjectName.Build.cs` file.
2.  In the file in which you will send your web request, include the `HttpModule.h` header file, the `HttpManager.h` header file, and the `HttpRetrySystem.h` file, as shown in the following code snippet:

include "Runtime/Online/HTTP/Public/HttpManager.h"

include "Runtime/Online/HTTP/Public/HttpModule.h"

include "Runtime/Online/HTTP/Public/HttpRetrySystem.h"


3.  Construct an `IHttpRequest` object from `FHttpModule` using the following code:

TSharedRef http=FHttpModule::Get().CreateRequest();


### Tip

`FHttpModule` is a singleton object. One copy of it exists for the entire program that you are meant to use for all interactions with the `FHttpModule` class.

4.  Attach your function to run to the `IHttpRequest` object's `FHttpRequestCompleteDelegate`, which has a signature as follows:

void HttpRequestComplete( FHttpRequestPtr request,

FHttpResponsePtr response, bool success );


5.  The delegate is found inside of the `IHttpRequest` object as `http->OnProcessRequestComplete()`:

FHttpRequestCompleteDelegate& delegate = http->OnProcessRequestComplete();


    There are a few ways to attach a callback function to the delegate. You can use the following:

    *   A lambda using `delegate.BindLambda()`:

委托.BindLambda(

// Anonymous, inlined code function (aka lambda)

[]( FHttpRequestPtr request, FHttpResponsePtr response, bool success ) -> void

{

UE_LOG( LogTemp, Warning, TEXT( "Http Response: %d, %s" ),

request->GetResponse()->GetResponseCode(),

*request->GetResponse()->GetContentAsString() );

});


    *   Any UObject's member function:

delegate.BindUObject( this, &AChapter12GameMode::HttpRequestComplete );


### Tip

You cannot attach to `UFunction` directly here as the `.BindUFunction()` command requests arguments that are all `UCLASS`, `USTRUCT` or `UENUM`.

    *   Any plain old C++ object's member function using `.BindRaw`:

PlainObject* plainObject = new PlainObject();

delegate.BindRaw( plainObject, &PlainObject::httpHandler );

// plainObject cannot be DELETED Until httpHandler gets called..


### Tip

You have to ensure that your `plainObject` refers to a valid object in memory at the time the HTTP request completes. This means that you cannot use `TAutoPtr` on `plainObject`, because that will deallocate `plainObject` at the end of the block in which it is declared, but that may be before the HTTP request completes.

    *   A global C-style static function:

// C-style function for handling the HTTP response:

void httpHandler( FHttpRequestPtr request,

FHttpResponsePtr response, bool success )

{

Info( "static: Http req handled" );

}

delegate.BindStatic( &httpHandler );


### Note

When using a delegate callback with an object, be sure that the object instance that you're calling back on lives on at least until the point at which the `HttpResponse` arrives back from the server. Processing the `HttpRequest` takes real time to run. It is a web request after all—think of waiting for a web page to load.

You have to be sure that the object instance on which you're calling the callback function has not deallocated on you between the time of the initial call and the invocation of your `HttpHandler` function. The object must still be in memory when the callback returns after the HTTP request completes.

You cannot simply expect that the `HttpResponse` function happens immediately after you attach the callback function and call `ProcessRequest()`! Using a reference counted `UObject` instance to attach the `HttpHandler` member function is a good idea to ensure that the object stays in memory until the HTTP request completes.

6.  Specify the URL of the site you'd like to hit:

http->SetURL( TEXT( "http://unrealengine.com" ) );


7.  Process the request by calling `ProcessRequest`:

http->ProcessRequest();


## How it works…

The HTTP object is all you need to send off HTTP requests to a server and get HTTP responses. You can use the HTTP request/response for anything that you wish; for example, submitting scores to a high scores table or to retrieve text to display in-game from a server.

They are decked out with a URL to visit and a function callback to run when the request is complete. Finally, they are sent off via `FManager`. When the web server responds, your callback is called and the results of your HTTP response can be shown.

## There's more…

You can set additional HTTP request parameters via the following member functions:

*   `SetVerb()` to change whether you are using the `GET` or `POST` method in your HTTP request
*   `SetHeaders()` to modify any general header settings you would like

# HTTP API – Progress bars

The `IHttpRequest` object from HTTP API will report HTTP download progress via a callback on a `FHttpRequestProgressDelegate` accessible via `OnRequestProgress()`. The signature of the function we can attach to the `OnRequestProgress()` delegate is as follows:

HandleRequestProgress( FHttpRequestPtr request, int32 sentBytes, int32 receivedBytes )


The three parameters of the function you may write include: the original `IHttpRequest` object, the bytes sent, and the bytes received so far. This function gets called back periodically until the `IHttpRequest` object completes, which is when the function you attach to `OnProcessRequestComplete()` gets called. You can use the values passed to your `HandleRequestProgress` function to advance a progress bar that you will create in UMG.

## Getting ready

You will need an internet connection to use this recipe. We will be requesting a file from a public server. You can use a public server or your own private server for your HTTP request if you'd like.

In this recipe, we will bind a callback function to just the `OnRequestProgress()` delegate to display the download progress of a file from a server. Have a project ready where we can write a piece of code that will perform `IHttpRequest,` and a nice interface on which to display percentage progress.

## How to do it…

1.  Link to the `UMG` and `HTTP` APIs in your `ProjectName.Build.cs` file.
2.  Build a small UMG UI with `ProgressBar` to display your HTTP request's progress.
3.  Construct an `IHttpRequest` object using the following code:

TSharedRef http = HttpModule::Get().CreateRequest();


4.  Provide a callback function to call when the request progresses, which updates a visual GUI element:

http->OnRequestProgress().BindLambda( []( FHttpRequestPtr request, int32 sentBytes, int32 receivedBytes ) -> void

{

int32 totalLen = request->GetResponse()->GetContentLength();

float perc = (float)receivedBytes/totalLen;

如果(HttpProgressBar)

HttpProgressBar->SetPercent( perc );

} );


1.  使用`http->ProcessRequest()`处理您的请求。

## 它是如何工作的...

`OnRequestProgress()`回调会定期触发,显示已发送和已接收的字节的 HTTP 进度。我们将通过计算`(float)receivedBytes/totalLen`来计算下载完成的总百分比,其中`totalLen`是 HTTP 响应的总字节长度。使用我们附加到`OnRequestProgress()`委托回调的 lambda 函数,我们可以调用 UMG 小部件的`.SetPercent()`成员函数来反映下载的进度。
posted @ 2024-05-15 15:26  绝不原创的飞龙  阅读(53)  评论(0编辑  收藏  举报