UE4运行时交互工具框架

Posted 新缸中之脑

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了UE4运行时交互工具框架相关的知识,希望对你有一定的参考价值。

在本文中,我将介绍很多内容。我提前为太长的篇幅道歉。但是,这篇文章的主题本质上是“如何使用虚幻引擎构建 3D 工具”,这是一个很大的话题。在本文结束时,我将介绍交互式工具框架,这是 Unreal Engine 4.26 中的一个系统,可以相对简单地构建多种类型的交互式 3D 工具。我将专注于“在运行时”使用这个框架,即在构建的游戏中。但是,我们使用这个完全相同的框架在虚幻编辑器中构建 3D 建模工具套件。而且,其中许多工具都可以在运行时直接使用!在你的游戏中雕刻!它太酷了。

下面有一个 ToolsFramworkDemo 应用程序的短视频和一些屏幕截图 - 这是一个构建的可执行文件,不在 UE 编辑器中运行(尽管也可以)。该演示允许你创建一组网格,可以通过单击进行选择(通过 shift-click/ctrl-click 支持多选),并为活动选择显示 3D 变换 Gizmo。左侧的一小组 UI 按钮用于执行各种操作。Add Bunny按钮将导入和附加一个兔子网格,Undo和Redo会按照你的预期进行。World按钮在World 和 Local 坐标系之间切换 Gizmo:

其余按钮启动各种建模工具,它们与 UE 4.26 编辑器的建模模式中使用的工具实现完全相同。PolyExtrude是绘制多边形工具,你可以在其中在 3D 工作平面上绘制一个封闭的多边形(可以通过 ctrl 单击重新定位),然后以交互方式设置挤出高度。PolyRevolve允许你在 3D 工作平面上绘制开放或封闭路径 - 双击或关闭路径到终点 - 然后编辑生成的旋转曲面。Edit Polygons是编辑器中的 PolyEdit 工具,在这里您可以选择面/边/顶点并使用 3D gizmo 移动它们 — 请注意,各种 PolyEdit 子操作,如 Extrude 和 Inset,不会在 UI 中公开,但可以工作。

所有这些几何图形是在演示中创建的。选择窗口并使用 GIZMO 旋转。
Plane Cut使用工作平面切割网格,Boolean执行网格布尔运算(需要两个选定对象)。Remesh重新对网格进行三角剖分(不幸的是,我无法轻松显示网格线框)。Vertex Sculpt允许您对顶点位置进行基本的 3D 雕刻,而DynaSculpt 进行自适应拓扑雕刻,这就是我在屏幕截图中展示的应用于 Bunny 的内容。最后,Accept和Cancel按钮应用或放弃当前的工具结果(这只是一个预览) - 我将在下面进一步解释。

兔子长出一些新的部分

这不是一个功能齐全的 3D 建模工具,它只是一个基本的演示。一方面,没有任何形式的保存或导出,不过,添加一个快速的 OBJ 导出并不难!不存在对分配材质的支持,您看到的材质是硬编码的或由工具自动使用,例如动态网格雕刻中的平面着色。同样,一个积极的 C++ 开发人员可以相对容易地添加类似的东西。2D 用户界面是一个非常基本的 UMG 用户界面。我假设这是一次性的,你将构建自己的 UI。再说一次,如果你想做一个非常简单的特定领域的建模工具,比如一个用于清理医学扫描的 3D 雕刻工具,你也许可以在稍加修改后摆脱这个 UI。

1、获取并运行示例项目

在开始之前,本教程适用于 UE 4.26,你可以从Epic Games Launcher安装它。本教程的项目位于 Github 上的 UnrealRuntimeToolsFrameworkDemo存储库(MIT 许可)。目前,该项目只能在 Windows 上运行,因为它依赖于MeshModelingToolset引擎插件,该插件目前仅适用于 Windows。让该插件在 OSX/Linux 上工作主要是选择性删除的问题,但它需要引擎源代码构建,这超出了本教程的范围。

进入顶级文件夹后,右键单击Windows 资源管理器中的ToolsFrameworkDemo.uproject ,然后从上下文菜单中选择Generate Visual Studio project files 。这将生成ToolsFrameworkDemo.sln,你可以使用它来打开 Visual Studio。也可以直接在编辑器中打开 .uproject — 它会要求编译,但可能需要参考 C++ 代码才能真正了解该项目中发生的情况。

构建解决方案并启动(按 F5),编辑器应打开到示例地图中。可以使用主工具栏中的大播放按钮在 PIE 中测试该项目,或者单击启动按钮来构建一个熟化的可执行文件。这将需要几分钟,之后构建的游戏将在单独的窗口中弹出。如果它以这种方式启动(我认为这是默认设置),可以点击 Escape 退出全屏。在全屏模式下,你必须按Alt+F4退出,因为没有菜单/UI。

2、概述

这篇文章太长了,需要一个目录。以下是我要介绍的内容:

首先,我将解释交互式工具框架(ITF) 作为一个概念的一些背景。它来自哪里,它试图解决什么问题。随意跳过这个 author-on-his-soapbox 部分,因为本文的其余部分不以任何方式依赖它。

接下来我将解释 UE4 交互工具框架的主要部分。我们将从工具、工具构建器和工具管理器开始,并讨论工具生命周期、接受/取消模型和基础工具。输入处理将在输入行为系统、通过工具属性集存储的工具设置和工具操作中进行介绍。

接下来我将解释Gizmos系统,用于实现视口内 3D 小部件,重点介绍上面剪辑/图像中显示的标准 UTransformGizmo 。

在 ITF 的最高级别,我们有Tools Context 和 ToolContext API,我将详细介绍 ITF 的客户端需要实现的 4 个不同的 API - IToolsContextQueriesAPI、IToolsContextTransactionsAPI、IToolsContextRenderAPI 和 IToolsContextAssetAPI。然后我们将介绍一些特定于网格编辑工具的细节,特别是Actor/Component Selections、FPrimitiveComponentTargets和FComponentTargetFactory。

到目前为止,一切都将与 UE4.26 附带的 ITF 模块有关。为了在运行时使用 ITF,我们将创建我们自己的运行时工具框架后端,其中包括一个基本的可选网格“场景对象”的 3D 场景、一个非常标准的 3D 应用程序变换 gizmo 系统以及 ToolsContext API 的实现 I上面提到的与这个运行时场景系统兼容的。本节主要解释了我们必须添加到 ITF 以在运行时使用它的额外位,因此您需要阅读前面的部分才能真正理解它。

接下来我将介绍一些特定于演示的材料,包括使演示工作所需的ToolsFrameworkDemo 项目设置、RuntimeGeometryUtils 更新,特别是对 USimpleDynamicMeshComponent 的碰撞支持,然后是一些关于在运行时使用建模模式工具的注释,因为这通常需要一些胶水代码才能使现有的网格编辑工具在游戏环境中发挥作用。

就是这样!让我们开始…

3、交互式工具框架 - 为什么

我不喜欢通过证明它的存在来开始一篇关于某事的文章的想法。但是,我想我需要。我花了很多年 - 基本上是我的整个职业生涯 - 构建 3D 创建/编辑工具。我的第一个系统是ShapeShop(它自 2008 年以来一直没有更新,但仍然可以工作——这是 Windows 向后兼容性的证明!)。我还构建了 Meshmixer,它成为 Autodesk 产品,下载数百万次,并被广泛使用至今。通过Twitter搜索,我不断惊讶于人们使用 Meshmixer 做的事情,很多数字牙医!!。我还构建了其他从未出现过的全功能系统,例如我们称之为手绘世界的 3D 透视草图界面 ,是我在 Autodesk Research 构建的。之后,我帮助构建了一些医疗 3D 设计工具,例如Archform 牙齿矫正器规划应用程序和NiaFit 小腿假肢插座设计工具(VR ),遗憾的是我在它有任何流行的希望之前就放弃了。

撇开自我祝贺不谈,在过去 15 多年制作这些 3D 工具的过程中,我学到的是,制造一个巨大的混乱是非常容易的。我开始研究后来成为 Meshmixer 的东西,因为 Shapeshop 已经到了无法添加任何东西的地步。然而,Shapeshop 的某些部分形成了一个非常早期的“工具框架”,我将其提取并用作其他各种项目的基础,甚至还有一些 Meshmixer(最终也变得非常脆弱!)。该代码仍在我的网站上。当我离开 Autodesk 时,我回到了如何构建工具的这个问题,并创建了frame3Sharp 库这使得在 C# 游戏引擎中构建运行时 3D 工具变得(相对)容易。这个框架围绕上面提到的 Archform、NiaFit 和 Cotangent 应用程序发展起来,并一直为它们提供动力。但是,后来我加入了 Epic,并重新开始使用 C++!

所以,这就是 UE4 交互式工具框架的起源故事。使用这个框架,一个小团队(6 人或更少的人,取决于月份)在 UE4 中构建了建模模式,它有 50 多个“工具”。有些非常简单,例如使用选项复制事物的工具,有些则非常复杂,例如整个 3D 雕刻工具。但关键点是,工具代码相对干净且很大程度上独立 - 几乎所有工具都是一个独立的 cpp/h 对。不是通过剪切和粘贴而独立,而是独立于这一点,我们尽可能地将“标准”工具功能移动到框架中,否则这些功能将不得不被复制。

3.1 让我们谈谈框架

我在解释交互式工具框架时遇到的一个挑战是我没有参考点来比较它。大多数 3D 内容创建工具在其代码库中都有一定程度的“工具框架”,但除非你尝试向 Blender 添加功能,否则可能从未与这些东西进行过交互。所以,我不能试图通过类比来解释。并且这些工具并没有真正努力提供类似的原型框架作为大写-F 框架。所以很难把握。(PS:如果您认为您知道类似的Framework,请联系并告诉我!)

但是,在其他类型的应用程序开发中,框架非常常见。例如,如果你想构建一个 Web 应用程序或移动应用程序,你几乎肯定会使用一个定义明确的框架,如 Angular 或 React 或本月流行的任何东西(实际上有数百个)。这些框架倾向于将“小部件”等低级方面与视图等高级概念混合在一起。我在这里关注视图,因为这些框架中的绝大多数都是基于视图的概念。通常,前提是你拥有数据,并且你希望将这些数据放入视图中,并带有一定数量的 UI,允许用户探索和操作该数据。甚至还有一个标准术语,“模型-视图-控制器”架构。XCode 界面生成器是我所知道的最好的例子,你实际上是在故事板上用户将看到的视图,并通过这些视图之间的转换来定义应用程序行为。我经常使用的每个手机应用程序都是这样工作的。

提高复杂性,我们有像 Microsoft Word 或 Keynote 这样的应用程序,它们与基于视图的应用程序完全不同。在这些应用程序中,用户将大部分时间花在单个视图中,并且直接操作内容而不是抽象地与数据交互。但大部分操作都是以Commands的形式进行的,例如删除文本或编辑Properties。例如,在 Word 中,当我不键入字母时,我通常要么将鼠标移动到命令按钮上以便我可以单击它——一个离散的操作——要么打开对话框并更改属性。我不做的是花费大量时间使用连续的鼠标输入(拖放和选择是明显的例外)。

现在考虑一个内容创建应用程序,如 Photoshop 或 Blender。同样,作为用户,您将大部分时间花在标准化视图中,并且你直接操作的是内容而不是数据。仍然有大量具有属性的命令和对话框。但是这些应用程序的许多用户——尤其是在创意环境中——也花费大量时间非常小心地在按住其中一个按钮的同时移动鼠标。此外,当他们这样做时,应用程序通常处于特定模式,其中鼠标移动(通常与修改热键结合使用)以特定模式的方式被捕获和解释。该模式允许应用程序在大量方式之间消除歧义,mouse-movement-with-button-held-down动作可以被解释,本质上是为了将捕获的鼠标输入引导到正确的位置。这与命令根本不同,命令通常是无模式的,并且在输入设备方面也是无状态的。

除了模式之外,内容创建应用程序的一个标志是我将称为Gizmos的东西,它们是附加的临时交互式视觉元素,它们不是内容的一部分,但提供了一种(半无模式)操作内容的方式。例如,可以单击拖动以调整矩形大小的矩形角上的小框或 V 形将是 Gizmo 的标准示例。这些通常被称为小部件,但我认为使用这个术语会让人感到困惑,因为它与按钮和菜单小部件重叠,所以我将使用 Gizmos。

所以,现在我可以开始暗示交互式工具框架的用途了。在最基本的层面上,它提供了一种系统的方法来实现捕获和响应用户输入的模态状态,为了简洁起见,我将其称为交互工具或工具,以及实现 Gizmos(我将假定它本质上是空间本地化的上下文敏感模式,但我们可以将讨论保存在 Twitter 上)。

3.2 为什么需要一个框架?

这是我被问过很多次的问题,主要是那些没有尝试构建复杂的基于工具的应用程序的人。简短的回答是,减少(但遗憾的是没有消除)你制造邪恶灾难的机会。但我也会做一个长的回答。

关于基于工具的应用程序需要了解的重要一点是,一旦你为用户提供以任何顺序使用工具的选项,他们就会这样做,这将使一切变得更加复杂。在基于视图的应用程序中,用户通常是“On Rails”,因为应用程序允许在 Y 之后而不是之前执行 X。当我启动 Twitter 应用程序时,我不能直接跳转到所有内容——我必须浏览一系列视图。这允许应用程序的开发人员对应用程序状态做出大量假设。特别是,尽管视图可能会操作相同的底层 DataModel(几乎总是某种形式的数据库),但我永远不必担心区分一个视图中的点击与另一个视图中的点击。在某种意义上,意见是模式,在特定视图的上下文中,通常只有命令,没有工具。

因此,在基于视图的应用程序中,谈论工作流非常容易。创建基于视图的应用程序的人往往会画很多类似这样的图表:

这些图可能是视图本身,但更多时候它们是用户通过应用程序所采取的步骤——如果你愿意的话,它们是用户故事。它们并不总是严格线性的,可能存在分支和循环(Google Image Search for Workflow 有很多更复杂的示例)。但总是有明确的进入和退出点。用户从一个任务开始,并通过工作流完成该任务。然后很自然地设计一个应用程序来提供用户可以完成任务的工作流。我们可以通过 Workflow 有意义地谈论 Progress,关联的 Data 和 Application State 也构成了一种 Progress。随着额外任务的添加,开发团队的工作是提出一种设计,以允许有效地完成这些必要的工作流程。

内容创建/编辑应用程序的根本复杂性在于,这种方法根本不适用于它们。我认为最终的区别在于内容创建/编辑工具中没有固有的进度概念。例如,作为 Powerpoint 用户,我可以(而且确实!)花几个小时重新组织我的幻灯片,调整图像大小和对齐方式,稍微调整文本。在我看来,我可能对进度有一些模糊的概念,但这并没有在应用程序中编码。我的任务在应用程序之外。如果没有明确的任务或进度衡量标准,就没有工作流程!

我认为内容创建/编辑应用程序更有用的心智模型就像右边的图像。绿色中央集线器是这些应用程序中的默认状态,通常你只是在其中查看你的内容。例如,在 Photoshop 中平移和缩放图像,或在 Blender 中浏览 3D 场景。这是用户花费大量时间的地方。蓝色辐条是工具。我会去一个工具一段时间,但我总是回到中心。

因此,如果我要随着时间的推移跟踪我的状态,那将是通过无数工具进出默认集线器的曲折路径。没有明确定义的顺序,作为用户,我通常可以按照我认为合适的任何顺序自由使用工具。在一个缩影中,我们可能能够找到定义明确的小型工作流来分析和优化,但在应用程序级别,工作流实际上是无限的。

看起来相对明显的是,你需要在此处采用的架构方法与在视图方法中的不同。通过以正确的方式眯眼看它,人们可能会争辩说每个工具基本上都是一个视图,那么这里真正不同的是什么?根据我的经验,不同之处在于我认为是Tool Sprawl。

如果你有明确定义的工作流程,那么很容易判断什么是必要的,什么是不必要的。与所需工作流程无关的功能不仅会浪费设计和工程时间,而且最终会使工作流程变得比必要的复杂——这会使用户体验变得更糟!现代软件开发的正统观念非常关注这个前提——构建最小可行的产品,然后迭代、迭代、迭代以消除用户的摩擦。

基于工具的应用程序根本不同,因为每增加一个工具都会增加应用程序的价值。如果我没有使用特定工具,那么除了启动该工具所需的附加工具栏按钮带来的小 UI 开销之外,它的添加几乎不会影响我。当然,学习新工具需要付出一些努力。但是,这种努力的回报是这个新工具现在可以与所有其他工具相结合!这导致了一种应用级网络效应,其中每个新工具都是所有现有工具的力量倍增器。如果观察几乎所有主要的内容创建/编辑工具,这一点就会立即显现出来,其中有无数的工具栏和工具栏菜单以及工具栏的嵌套选项卡,隐藏在其他工具栏后面。对局外人来说,这看起来很疯狂,但对用户来说,

许多来自面向工作流的软件世界的人都惊恐地看着这些应用程序。我观察到许多新项目,其中团队开始尝试构建一些“简单”的东西,专注于“核心工作流程”,也许是为“新手用户”绘制的,并且绘制了许多漂亮的线性工作流程图。但现实情况是,新手用户在掌握你的应用程序之前只是新手,然后他们会立即要求更多功能。因此,你将在这里和那里添加一个工具。几年后,你将拥有一套庞大的工具,如果没有系统的方法来组织它们,手上就会一团糟。

3.3 遏制伤害

混乱从何而来?据我所见,有几种常见的惹麻烦的方法。首先是低估了手头任务的复杂性。许多内容创建应用程序以“查看器”开始,其中所有应用程序逻辑(如 3D 相机控件)都直接在鼠标和 UI 按钮处理程序中完成。然后随着时间的推移,只需添加更多 if/else 分支或 switch case,就可以合并新的编辑功能。这种方法可以持续很长时间,而且我工作过的许多 3D 应用程序的核心仍然是这些残留的代码分支。但是你只是在挖掘一个更深的代码洞并用代码意大利面填充它。最终,将需要一些实际的软件架构,并且需要进行痛苦的重构工作(随后是多年的修复回归,

即使有一定数量的“工具架构”,如何处理设备输入也很棘手,而且往往最终导致混乱的架构锁定。鉴于“工具”通常由设备输入驱动,一个看似显而易见的方法是直接为工具提供输入事件处理程序,如 OnMouseUp/OnMouseMove/OnMouseDown 函数。这成为放置“做事”代码的自然位置,例如在鼠标事件上,你可以直接在绘画工具中应用画笔印章。在用户要求支持其他输入设备(如触摸、笔或 VR 控制器)之前,这似乎是无害的。怎么办?只是将呼叫转发给鼠标处理程序吗?压力或 3D 位置呢?然后是自动化,当用户开始要求能够为你的工具编写脚本时。它不是。绝对不。真的,不要)。

将重要代码放入输入事件处理程序还会导致诸如标准事件处理模式的猖獗复制粘贴之类的事情,如果需要进行更改,这可能会很乏味。而且,昂贵的鼠标事件处理程序实际上会使您的应用程序感觉不如应有的响应,这是由于称为鼠标事件优先级的东西。所以,你真的要小心处理工具架构的这一部分,因为看似标准的设计模式可能会引发一系列问题。

同时,如果工具架构定义过于严格,它可能成为扩展工具集的障碍,因为新的需求不“符合”初始设计的假设。如果许多工具都建立在初始架构之上,那么更改就变得棘手,然后聪明的工程师被迫想出变通办法,现在你有两个(或更多)工具架构。最大的挑战之一就是如何在工具实现和框架之间划分职责。

我不能声称交互式工具框架 (ITF) 会为你解决这些问题。最终,任何成功的软件最终都会被早期的设计决策所困,在这些决策之上已经建造了高山,而改变路线只能付出巨大的代价。我可以整天给你讲故事,关于我是如何对自己做到这一点的。我能说的是,在 UE4 中实现的 ITF 希望能从我过去的错误中受益。在过去的 2 年中,我们使用 ITF 在 UE4 编辑器中构建新工具的经验(到目前为止)相对轻松,我们一直在寻找消除任何摩擦点的方法。

4、工具、工具构建器和工具管理器

如上所述,交互工具是应用程序的模态状态,在此期间可以以特定方式捕获和解释设备输入。在交互式工具框架 (ITF) 中,UInteractiveTool基类表示模态状态,并具有你可能需要实现的非常小的 API 函数集。下面我总结了 psuedo-C++ 中的核心 UInteractiveTool API — 为简洁起见,我省略了虚拟、常量、可选参数等内容。我们稍后会在一定程度上介绍其他 API 函数集,但这些是关键的。在::Setup()中初始化您的工具,并在::Shutdown()中进行任何最终确定和清理,这也是你执行“应用”操作之类的地方。EToolShutdownType与HasAccept()和CanAccept()函数有关,我将在下面详细解释。最后,工具将有机会渲染()并勾选每一帧。请注意,还有一个 ::Tick() 函数,但你应该重写::OnTick()因为基类 ::Tick() 具有必须始终运行的关键功能。

UCLASS()
class UInteractiveTool : public UObject, public IInputBehaviorSource

    void Setup();
    void Shutdown(EToolShutdownType ShutdownType);
    void Render(IToolsContextRenderAPI* RenderAPI);
    void OnTick(float DeltaTime);

    bool HasAccept();
    bool CanAccept();
;

UInteractiveTool 不是一个独立的对象,你不能简单地自己生成一个。为了使其发挥作用,必须调用 Setup/Render/Tick/Shutdown,并传递诸如IToolsContextRenderAPI之类的适当实现,从而允许工具绘制线条/等。我将在下面进一步解释。但是现在你需要知道的是,要创建一个 Tool 实例,你需要从UInteractiveToolManager请求一个。要允许 ToolManager 构建任意类型,您需要向 ToolManager 注册一个 <String, UInteractiveToolBuilder > 对。UInteractiveToolBuilder 是一个非常简单的工厂模式基类,必须为每种工具类型实现:

UCLASS()
class UInteractiveToolBuilder : public UObject

    bool CanBuildTool(const FToolBuilderState& SceneState);
    UInteractiveTool* BuildTool(const FToolBuilderState& SceneState);
;

UInteractiveToolManager的主要 API总结如下。通常,你不需要实现自己的 ToolManager,基本实现功能齐全,应该完成使用工具所需的一切。但如有必要,你可以自由扩展子类中的各种功能。

下面的函数大致按照你调用它们的顺序列出。RegisterToolType()将字符串标识符与 ToolBuilder 实现相关联。然后应用程序使用SelectActiveToolType()设置一个活动的生成器,然后使用ActivateTool()创建一个新的 UInteractiveTool 实例。有 getter 可以访问活动工具,但实际上很少有人经常调用。应用程序必须在每一帧调用 Render() 和 Tick() 函数,然后应用程序调用活动工具的相关函数。最后DeactiveTool()用于终止活动工具。

UCLASS()
class UInteractiveToolManager : public UObject, public IToolContextTransactionProvider

    void RegisterToolType(const FString& Identifier, UInteractiveToolBuilder* Builder);
    bool SelectActiveToolType(const FString& Identifier);
    bool ActivateTool();

    void Tick(float DeltaTime);
    void Render(IToolsContextRenderAPI* RenderAPI);

    void DeactivateTool(EToolShutdownType ShutdownType);
;

4.1 工具生命周期

在高层次上,工具的生命周期如下

  • ToolBuilder 注册到 ToolManager
  • 一段时间后,用户表示他们希望启动工具(例如通过按钮)
  • UI 代码集 Active ToolBuilder,请求工具激活
  • ToolManager 检查 ToolBuilder.CanBuildTool() = true,如果是,则调用 BuildTool() 创建新实例
  • ToolManager 调用 Tool Setup()
  • 直到 Tool 被停用,它是 Tick()'d 和 Render()'d 每一帧
  • 用户表示他们希望退出工具(例如通过按钮、热键等)
  • ToolManager 使用适当的关闭类型调用 Tool Shutdown()
  • 一段时间后,工具实例被垃圾收集

注意最后一步。工具是 UObject,因此你不能依赖 C++ 析构函数进行清理。你应该在 Shutdown() 实现中进行任何清理,例如销毁临时参与者。

4.2 EToolShutdownType 和接受/取消模型

工具可以以两种不同的方式支持终止,具体取决于工具支持的交互类型。更复杂的替代方案是可以接受 — EToolShutdownType::Accept 或取消 EToolShutdownType::Cancel 的工具。这通常在工具的交互支持某种操作的实时预览时使用,用户可能希望放弃该操作。例如,将网格简化算法应用于选定网格的工具可能具有用户可能希望探索的一些参数,但如果探索不令人满意,则用户可能更愿意根本不应用简化。在这种情况下,UI 可以提供按钮来接受或取消活动工具,这会导致使用适当的 EToolShutdownType 值调用 ToolManager::DeactiveTool()。

第二个终止选项 - EToolShutdownType::Completed - 更简单,因为它只是指示工具应该“退出”。这种类型的终止可用于处理没有明确的“接受”或“取消”操作的情况,例如在简单可视化数据的工具中,增量应用编辑操作的工具(例如基于点击点生成对象),等等。

需要明确的是,你在使用 ITF 时不需要使用或支持接受/取消式工具。这样做通常会导致更复杂的 UI。如果你在应用程序中支持 Undo,那么即使是具有 Accept 和 Cancel 选项的 Tools,也可以等效为 Complete-style Tools,如果用户不满意,也可以 Undo。但是,如果工具完成可能涉及冗长的计算或以某种方式具有破坏性,则支持接受/取消往往会带来更好的用户体验。在 UE 编辑器的建模模式中,我们通常在编辑静态网格体资源时使用 Accept/Cancel 正是出于这个原因。

你必须做出的另一个决定是如何处理工具的模态性质。通常,将用户视为“处于”工具中是有用的,即处于特定的模态状态。那么他们是如何“走出去”的呢?您可以要求用户明确单击接受/取消/完成按钮以退出活动工具,这是最简单和最明确的,但确实意味着需要单击,并且用户必须在心理上意识到并管理此状态。或者,当用户在工具工具栏/菜单/等中选择另一个工具时(例如),你可以自动接受/取消/完成。然而,这引发了一个棘手的问题,即应该自动接受还是自动取消。这个问题没有正确答案,你必须决定什么最适合你的特定环境 —虽然根据我的经验,当一个人意外误点击时,自动取消可能会非常令人沮丧!

4.3 基础工具

ITF 的主要目标之一是减少编写工具所需的样板代码量,并提高一致性。几个“工具模式”出现得如此频繁,以至于我们在 ITF 的 /BaseTools/ 子文件夹中包含了它们的标准实现。基本工具通常包括一个或多个 InputBehaviors(见下文),其操作映射到您可以覆盖和实现的虚拟功能。我将简要介绍这些基本工具中的每一个,因为它们既是构建您自己的工具的有用方式,也是如何做事的示例代码的良好来源:

USingleClickTool捕获鼠标单击输入,如果IsHitByClick()函数返回有效点击,则调用OnClicked()函数。您提供这两个的实现。请注意,此处的FInputDeviceRay结构包括 2D 鼠标位置和 3D 射线。

class INTERACTIVETOOLSFRAMEWORK_API USingleClickTool : public UInteractiveTool

    FInputRayHit IsHitByClick(const FInputDeviceRay& ClickPos);
    void OnClicked(const FInputDeviceRay& ClickPos);
;

UClickDragTool捕获并转发连续的鼠标输入,而不是单击。如果CanBeginClickDragSequence()返回 true —通常你会在此处进行命中测试,类似于 USingleClickTool,则将调用 OnClickPress() / OnClickDrag() / OnClickRelease(),类似于标准 OnMouseDown/Move/Up 事件模式。但是请注意,你必须在OnTerminateDragSequence()中处理序列中止但没有释放的情况。

class INTERACTIVETOOLSFRAMEWORK_API UClickDragTool : public UInteractiveTool

    FInputRayHit CanBeginClickDragSequence(const FInputDeviceRay& PressPos);
    void OnClickPress(const FInputDeviceRay& PressPos);
    void OnClickDrag(const FInputDeviceRay& DragPos);
    void OnClickRelease(const FInputDeviceRay& ReleasePos);
    void OnTerminateDragSequence();
;

UMeshSurfacePointTool与 UClickDragTool 相似之处在于它提供了单击-拖动-释放输入处理模式。但是,UMesSurfacePointTool 假定它正在作用于一个目标 UPrimitiveComponent —它是如何获取这个 Component 的将在下面解释。下面HitTest()函数的默认实现将使用标准 LineTraces — 因此,如果足够的话,你不必重写此函数。UMeshSurfacePointTool 还支持悬停,并跟踪 Shift 和 Ctrl 修饰键的状态。对于简单的“表面绘图”类型工具,这是一个很好的起点,许多建模模式工具派生自 UMeshSurfacePointTool — 一个小提示:这个类也支持阅读手写笔压力,但是在 UE4.26 手写笔输入是 Editor-Only。

附注:虽然命名为 UMeshSurfacePointTool,但其实并不需要Mesh,只需要一个支持LineTrace的UPrimitiveComponent

class INTERACTIVETOOLSFRAMEWORK_API UMeshSurfacePointTool : public UInteractiveTool

    bool HitTest(const FRay& Ray, FHitResult& OutHit);
    void OnBeginDrag(const FRay& Ray);
    void OnUpdateDrag(const FRay& Ray);
    void OnEndDrag(const FRay& Ray);

    void OnBeginHover(const FInputDeviceRay& DevicePos);
    bool OnUpdateHover(const FInputDeviceRay& DevicePos);
    void OnEndHover();
;

还有第四个基础工具,UBaseBrushTool,它扩展了 UMeshSurfacePointTool,具有各种特定于基于画笔的 3D 工具的功能,即表面绘画笔刷、3D 雕刻工具等。这包括一组标准画笔属性、一个 3D 画笔位置/大小/衰减指示器、“画笔印记”跟踪以及各种其他有用的位。如果你正在构建画笔式工具,可能会发现这很有用。

4.4 FToolBuilder状态

UInteractiveToolBuilder API 函数都采用 FToolBuilderState 参数。此结构的主要目的是提供选择信息 - 它指示工具将或应该采取的行动。结构的关键字段如下所示。ToolManager 将构造一个 FToolBuilderState 并将其传递给 ToolBuilders,然后 ToolBuilders 将使用它来确定它们是否可以对 Selection 进行操作。在 UE4.26 ITF 实现中,Actor 和 Components 都可以传递,但也只能传递 Actor 和 Components。请注意,如果一个组件出现在 SelectedComponents 中,那么它的 Actor 将在 SelectedActors 中。包含这些 Actor 的 UWorld 也包括在内。

struct FToolBuilderState

    UWorld* World;
    TArray<AActor*> SelectedActors;
    TArray<UActorComponent*> SelectedComponents;
;

在建模模式工具中,我们不直接对组件进行操作,我们将它们包装在一个标准容器中,这样我们就可以,例如,3D 雕刻具有容器实现的“任何”网格组件。这在很大程度上是我可以编写本教程的原因,因为我可以让这些工具编辑其他类型的网格,例如运行时网格。但是在构建自己的工具时,你可以随意忽略 FToolBuilderState。你的 ToolBuilder 可以使用任何其他方式来查询场景状态,并且你的工具不限于作用于 Actor 或组件。

4.5 关于工具构建器

ITF 用户经常提出的一个问题是 UInteractiveToolBuilder 是否必要。在最简单的情况下,也就是最常见的情况下,你的 ToolBuilder 将是简单的样板代码 —不幸的是,因为它是一个 UObject,这个样板不能直接转换为 C++ 模板。当人们开始重新利用现有的 UInteractiveTool 实现来解决不同的问题时,ToolBuilders 的实用程序就会出现。

例如,在 UE 编辑器中,我们有一个用于编辑网格多边形组(实际上是多边形)的工具,称为 PolyEdit。我们还有一个非常相似的工具用于编辑网格三角形,称为 TriEdit。在引擎盖下,这些是相同的 UInteractiveTool 类。在 TriEdit 模式下,Setup() 函数将工具的各个方面配置为适合三角形。为了在 UI 中公开这两种模式,我们使用了两个独立的 ToolBuilder,它们在创建的 Tool 实例被分配之后、Setup() 运行之前设置了一个“bIsTriangleMode”标志。

我当然不会声称这是一个优雅的解决方案。但是,这是权宜之计。根据我的经验,随着你的工具集不断发展以处理新情况,这种情况总是会出现。通常可以通过一些自定义初始化、一些附加选项/属性等来填充现有工具来解决新问题。在理想世界中,人们会重构工具以通过子类化或组合来实现这一点,但我们很少生活在理想世界中。因此,破解工具以完成第二项工作所需的一些难看的代码可以放置在自定义 ToolBuilder 中,并(相对)封装在其中。

使用 ToolManager 注册 ToolBuilder 的基于字符串的系统可以允许你的 UI 级别(即按钮处理程序等)启动工具,而无需实际了解 Tool 类类型。这通常可以在构建 UI 时实现更清晰的关注点分离。例如,在我将在下面描述的 ToolsFrameworkDemo 中,工具是由 UMG 蓝图小部件启动的,它们只是将字符串常量传递给 BP 函数——它们根本不了解工具系统。 然而,在生成工具之前需要设置一个“活动”构建器有点像退化的肢体,这些操作可能会在未来结合起来。

5、输入行为系统

上面我说过“交互式工具是应用程序的模态状态,在此期间可以以特定方式捕获和解释设备输入”。但是 UInteractiveTool API 没有任何鼠标输入处理函数。这是因为输入处理(大部分)与工具分离。输入由工具创建并注册到UInputRouter的UInputBehavior对象捕获和解释, UInputRouter “拥有”输入设备并将输入事件路由到适当的行为。

这种分离的原因是绝大多数输入处理代码都是剪切和粘贴的,在特定交互的实现方式上略有不同。例如考虑一个简单的按钮点击交互。在一个常见的事件 API 中,您将拥有可以实现的 OnMouseDown()、OnMouseMove() 和 OnMouseUp() 等函数,假设你希望将这些事件映射到单个 OnClickEvent() 处理程序,以便按下按钮-释放动作。数量惊人的应用程序(尤其是 Web 应用程序)会触发 OnMouseDown 中的点击——这是错误的!但是,在 OnMouseUp 中盲目地触发 OnClickEvent 也是错误的!这里的正确行为实际上是相当复杂的。在 OnMouseDown() 中,你必须对按钮进行点击测试,并开始捕获鼠标输入。在 OnMouseUp 中,你必须点击测试按钮再次,如果光标仍在点击按钮,则仅触发 OnClickEvent。这允许取消点击,并且是所有严肃的 UI 工具包如何实现它(试试看!)。

如果你甚至拥有数十个工具,那么实现所有这些处理代码,特别是针对多个设备,将变得非常容易出错。因此,出于这个原因,ITF 将这些小的输入事件处理状态机移动到 UInputBehavior 实现中,这些实现可以在许多工具之间共享。事实上,一些简单的行为,如USingleClickInputBehavior、UClickDragBehavior和UHoverBehavior 可以处理大多数鼠标驱动交互的情况。然后,行为通过工具或 Gizmo 等可以实现的简单接口将其提炼的事件转发到目标对象。例如 USingleClickInputBehavior 可以作用于任何实现 IClickBehaviorTarget 的东西,它只有两个函数 - IsHitByClick() 和 OnClicked()。请注意,由于 InputBehavior 不知道它作用于什么——“按钮”可以是 2D 矩形或任意 3D 形状——Target 接口必须提供命中测试功能。

InputBehavior 系统的另一个方面是工具不直接与 UInputRouter 对话。他们只提供他们希望激活的 UInputBehavior 的列表。UInteractiveTool API 添加的支持此功能如下所示。通常,在工具的 ::Setup() 实现中,会创建和配置一个或多个输入行为,然后将其传递给 AddInputBehavior。然后,ITF 在必要时调用 GetInputBehaviors,将这些行为注册到 UInputRouter。注意:目前 InputBehavior 集不能在工具期间动态更改,但是您可以配置您的 Behaviors 以根据您希望的任何标准忽略事件。

class UInteractiveTool : public UObject, public IInputBehaviorSource

    // ...previous functions...

    void AddInputBehavior(UInputBehavior* Behavior);
    const UInputBehaviorSet* GetInputBehaviors();
;

UInputRouter与UInteractiveToolManager的相似之处在于默认实现足以满足大多数用途。InputRouter 的唯一工作是跟踪所有活动的 InputBehavior 并调解捕获的输入设备。捕获是工具中输入处理的核心。当 MouseDown 事件进入 InputRouter 时,它会检查所有已注册的 Behaviors 以询问它们是否要开始捕获鼠标事件流。例如,如果您按下一个按钮,该按钮注册的 USingleClickInputBehavior 将表明是的,它想要开始捕获。一次只允许单个行为捕获输入,并且可能需要捕获多个行为(彼此不了解) - 例如,与当前视图重叠的 3D 对象。因此,每个 Behavior 返回一个 FInputCaptureRequest,指示“是”或“否”以及深度测试和优先级信息。UInputRouter 然后查看所有捕获请求,并根据深度排序和优先级,选择一个行为并告诉它捕获将开始。然后 MouseMove 和 MouseRelease 事件仅传递给该行为,直到 Capture 终止(通常在 MouseRelease 上)。

实际上,在使用 ITF 时,你很少需要与 UInputRouter 交互。一旦建立了应用程序级鼠标事件和 InputRouter 之间的连接,你就不需要再次触摸它了。该系统主要处理常见错误,例如由于捕获出错而导致鼠标处理“卡住”,因为 UInputRouter 最终控制鼠标捕获,而不是单个行为或工具。在随附的 ToolsFrameworkDemo 项目中,我已经实现了 UInputRouter 运行所需的一切。

基本的 UInputBehavior API 如下所示。FInputDeviceState是一个大型结构,包含给定事件/时间的所有输入设备状态,包括常用修饰键的状态、鼠标按钮状态、鼠标位置等。与许多输入事件的一个主要区别是还包括与输入设备位置相关的 3D 世界空间射线。

UCLASS()
class UInputBehavior : public UObject

    FInputCapturePriority GetPriority();
    EInputDevices GetSupportedDevices();

    FInputCaptureRequest WantsCapture(const FInputDeviceState& InputState);
    FInputCaptureUpdate BeginCapture(const FInputDeviceState& InputState);
    FInputCaptureUpdate UpdateCapture(const FInputDeviceState& InputState);
    void ForceEndCapture(const FInputCaptureData& CaptureData);

    // ... hover support...

我在上面的 API 中省略了一些额外的参数,以简化事情。特别是如果你实现自己的行为,你会发现几乎到处都有一个 EInputCaptureSide 枚举,主要作为默认的 EInputCaptureSide::Any。这是为了将来使用,以支持行为可能特定于任一手的 VR 控制器的情况。

但是,对于大多数应用程序,你可能会发现实际上不必实现自己的行为。一组标准行为,例如上面提到的那些,包含在 InteractiveToolFramework 模块的 /BaseBehaviors/ 文件夹中。大多数标准行为都是从基类UAnyButtonInputBehavior 派生的,它允许它们使用任何鼠标按钮,包括由 TFunction(可能是键盘键)定义的“自定义”按钮!类似地,标准 BehaviorTarget 实现都派生自IModifierToggleBehaviorTarget,它允许在 Behavior 上配置任意修饰键并将其转发到 Target,而无需子类化或修改 Behavior 代码。

直接使用 UInputBehaviors

在上面的讨论中,我重点讨论了 UInteractiveTool 提供 UInputBehaviorSet 的情况。Gizmos 将类似地工作。但是,UInputRouter 本身根本不知道 Tools,完全可以单独使用 InputBehavior 系统。在 ToolsFrameworkDemo 中,我在USceneObjectSelectionInteraction类中以这种方式实现了点击选择网格交互。这个类实现了 IInputBehaviorSource 和 IClickBehaviorTarget 本身,并且只属于框架后端子系统。即使这不是绝对必要的 - 您可以直接使用 UInputRouter 注册您自己创建的 UInputBehavior (但是请注意,由于我对 API 的疏忽,在 UE4.26 中您无法显式注销单个行为,您只能通过源注销)。

5.1 非鼠标输入设备

UE4.26 ITF 实现中当前未处理其他设备类型,但是 frame3Sharp 中此行为系统的先前迭代支持触摸和 VR 控制器输入,并且这些应该(最终)在 ITF 设计中类似地工作。一般的想法是只有 InputRouter 和 Behaviors 需要明确了解不同的输入模式。IClickBehaviorTarget 实现应该与鼠标按钮、手指点击或 VR 控制器点击类似地工作,但也不排除为特定于设备的交互(例如,来自两指捏合、空间控制器手势等)定制的额外行为目标. 工具可以为不同的设备类型注册不同的行为,InputRouter 将负责处理哪些设备是活动的和可捕获的。

目前,可以通过映射到鼠标事件来完成对其他设备类型的某种程度的处理。由于 InputRouter 不直接监听输入事件流,而是由 ITF 后端创建和转发事件,这是做这种映射的自然场所,下面将解释更多细节。

5.2 限制 - 捕获中断

在设计交互时需要注意的这个系统的一个重要限制是,框架尚不支持主动捕获的“中断”。当人们希望进行单击或拖动的交互时,这种情况最常见,具体取决于鼠标是立即在同一位置释放还是移动了某个阈值距离。在简单的情况下,这可以通过UClickDragBehavior处理,由你的 IClickDragBehaviorTarget 实现做出决定。但是,如果单击和拖动动作需要去到彼此不知道的非常不同的地方,这可能会很痛苦。支持这种交互的一种更简洁的方法是允许一个 UInputBehavior “中断”另一个 - 在这种情况下,当满足先决条件(即足够的鼠标移动)时,拖动以“中断”单击的活动捕获。这是 ITF 未来可能会改进的一个领域。

6、工具属性集

UInteractiveTool 还有一组我没有介绍的 API 函数,用于管理一组附加的UInteractiveToolPropertySet对象。这是一个完全可选的系统,在某种程度上是为在 UE 编辑器中使用而量身定制的。对于运行时使用,它不太有效。本质上,UInteractiveToolPropertySet 用于存储你的工具设置和选项。它们是具有 UProperties 的 UObject,在编辑器中,这些 UObject 可以添加到 Slate DetailsView 以在编辑器 UI 中自动公开这些属性。

额外的 UInteractiveTool API 总结如下。一般在Tool ::Setup()函数中,会创建各种UInteractiveToolPropertySet子类并传递给AddToolPropertySource()。ITF 后端将使用 GetToolProperties() 函数初始化 DetailsView 面板,然后 Tool 可以使用 SetToolPropertySourceEnabled() 动态显示和隐藏属性集

class UInteractiveTool : public UObject, public IInputBehaviorSource

    // ...previous functions...
public:
    TArray<UObject*> GetToolProperties();
protected:
    void AddToolPropertySource(UObject* PropertyObject);
    void AddToolPropertySource(UInteractiveToolPropertySet* PropertySet);
    bool SetToolPropertySourceEnabled(UInteractiveToolPropertySet* PropertySet, bool bEnabled);
;

在 UE 编辑器中,可以使用元标记来标记 UProperties 以控制生成的 UI 小部件 - 例如滑块范围、有效整数值以及基于其他属性的值启用/禁用小部件。建模模式中的大部分 UI 都是以这种方式工作的。

不幸的是,UProperty 元标记在运行时不可用,并且 UMG 小部件不支持 DetailsView 面板。结果,ToolPropertySet 系统变得不那么引人注目了。不过,它仍然提供了一些有用的功能。一方面,属性集支持使用属性集的 SaveProperties() 和 RestoreProperties() 函数跨工具调用保存和恢复其设置。您只需在 Tool Shutdown() 中设置的每个属性上调用 SaveProperties(),并在 ::Setup() 中调用 RestoreProperties()。

第二个有用的功能是 WatchProperty() 函数,它允许响应 PropertySet 值的更改而无需任何类型的更改通知。这对于 UObject 是必要的,因为 C++ 代码可以直接更改 UObject 上的 UProperty,这不会导致发送任何类型的更改通知。因此,可靠检测此类更改的唯一方法是通过轮询。是的,投票。这并不理想,但请考虑 (1) 工具必须具有有限数量的用户可以处理的属性,以及 (2) 一次只有一个工具处于活动状态。为了让您不必为 ::OnTick() 中的每个属性实现存储值比较,您可以使用以下模式添加观察者:

MyPropertySet->WatchProperty( MyPropertySet->bBooleanProp, [this](bool bNewValue) // handle change! );
在 UE4.26 中,有一些额外的警告(阅读:错误)必须解决,请参阅下文了解更多详细信息。

7、工具操作

最后,UInteractiveTool API 的最后一个主要部分是对Tool Actions的支持。这些在建模模式工具集中没有广泛使用,除了实现热键功能。但是,工具操作与热键没有特别的关系。它们允许工具公开可以通过整数标识符调用的“动作”(即无参数函数)。Tool 构造并返回一个FInteractiveToolActionSet,然后更高级别的客户端代码可以枚举这些操作,并使用下面定义的ExecuteAction函数执行它们。

class UInteractiveTool : public UObject, public IInputBehaviorSource

    // ...previous functions...
public:
    FInteractiveToolActionSet* GetActionSet();
    void ExecuteAction(int32 ActionID);
protected:
    void RegisterActions(FInteractiveToolActionSet& ActionSet);
;

下面的示例代码显示了两个正在注册的工具操作。请注意,尽管FInteractiveToolAction包含热键和修饰符,但这些只是对更高级别客户端的建议。UE 编辑器查询操作的工具,然后将建议的热键注册为编辑器热键,这允许用户重新映射它们。UE在运行时没有任何类似的热键系统,您需要自己手动映射这些热键

void UDynamicMeshSculptTool::RegisterActions(FInteractiveToolActionSet& ActionSet)

    ActionSet.RegisterAction(this, (int32)EStandardToolActions::BaseClientDefinedActionID + 61,
        TEXT("SculptDecreaseSpeed"),
        LOCTEXT("SculptDecreaseSpeed", "Decrease Speed"),
        LOCTEXT("SculptDecreaseSpeedTooltip", "Decrease Brush Speed"),
        EModifierKey::None, EKeys::W,
        [this]()  DecreaseBrushSpeedAction(); );

    ActionSet.RegisterAction(this, (int32)EStandardToolActions::ToggleWireframe,
        TEXT("ToggleWireframe"),
        LOCTEXT("ToggleWireframe", "Toggle Wireframe"),
        LOCTEXT("ToggleWireframeTooltip", "Toggle visibility of wireframe overlay"),
        EModifierKey::Alt, EKeys::W,
        [this]()  ViewProperties->bShowWireframe = !ViewProperties->bShowWireframe; );

最终,每个 ToolAction 有效负载都存储为 TFunction<void()>。如果你只是转发到另一个 Tool 函数,比如上面的 DecreaseBrushSpeedAction() 调用,你不一定受益于 ToolAction 系统,根本不需要使用它。然而,由于当前工具暴露于蓝图的限制,ToolActions(因为它们可以通过一个简单的整数调用)可能是一种将工具功能暴露给 BP 的有效方法,而无需编写许多包装函数。

8、小玩意儿

正如我所提到的,“Gizmo”是指我们在 2D 和 3D 内容创建/编辑应用程序中使用的那些在视口内点击的小东西,可以让你有效地操纵视觉元素或对象的参数。例如,如果您使用过任何 3D 工具,那么你几乎肯定使用过标准的平移/旋转/缩放 Gizmo。与工具一样,Gizmo 捕获用户输入,但不是完整的 Modal 状态,Gizmo 通常是瞬态的,即 Gizmo 可以来来去去,并且你可以同时激活多个 Gizmo,它们仅在你单击时捕获输入“开”他们(“开”的意思可能有点模糊)。正因为如此,Gizmo 通常需要一些特定的可视化表示,以允许用户指示他们何时想要“使用”Gizmo,但从概念上讲,你也可以拥有基于热键或应用程序状态(例如复选框)执行此操作的 Gizmo。

在 Interactive Tools Framework 中,Gizmo 被实现为UInteractiveGizmo的子类,它与 UInteractiveTool 非常相似:

UCLASS()
class UInteractiveGizmo : public UObject, public IInputBehaviorSource

    void Setup();
    void Shutdown();
    void Render(IToolsContextRenderAPI* RenderAPI);
    void Tick(float DeltaTime);

    void AddInputBehavior(UInputBehavior* Behavior);
    const UInputBehaviorSet* GetInputBehaviors();

同样,Gizmo 实例由UInteractiveGizmoManager管理,使用通过字符串注册的UInteractiveGizmoBuilder工厂。Gizmo 使用相同的 UInputBehavior 设置,并且由 ITF 每帧进行类似渲染和勾选。

在这个高层次上,UInteractiveGizmo 只是一个骨架,要实现自定义 Gizmo,你必须自己做很多工作。与工具不同,提供“基础”小玩意儿更具挑战性,因为它具有视觉表示方面。特别是,标准的 InputBehaviors 将要求你能够对 Gizmo 进行光线投射命中测试,因此不能只在 Render() 函数中绘制任意几何图形。也就是说,ITF 确实提供了一个非常灵活的标准 Translate-Rotate-Scale Gizmo 实现,可以重新利用它来解决许多问题。

8.1 标准 UTransformGizmo

如果 ITF 不包含标准的平移-旋转-缩放 (TRS) Gizmos,那么将 ITF 称为构建 3D 工具的框架将是非常有问题的。目前在 UE4.26 中可用的是一个名为UTransformGizmo的组合 TRS Gizmo(右侧屏幕截图) ,它支持轴和平面平移(轴线和中心人字形)、轴旋转(圆)、统一比例(中心框)、轴比例(外轴括号)和平面刻度(外人字形)。这些子 Gizmo 可以单独配置,因此你可以(例如)通过将某些枚举值传递给 Gizmo 构建器来创建仅具有 XY 平面平移和 Z 旋转的 UTransformGizmo 实例。

这个 TRS Gizmo 不是一个单一的整体 Gizmo,它是由一组可以重新用于许多其他用途的部件组成的。这个子系统足够复杂,值得单独写一篇文章,但总而言之,我上面提到的 UTransformGizmo 的每个元素实际上都是一个单独的 UInteractiveGizmo(所以,是的,你可以有嵌套/分层 Gizmo,你可以继承 UTransformGizmo 来添加额外的自定义控件)。例如,轴平移子 Gizmo(绘制为红/绿/蓝线段)是UAxisPositionGizmo的实例,旋转圆是UAxisAngleGizmo。

像 UAxisPositionGizmo 这样的子 Gizmo 并没有显式地绘制上图中的线条。相反,它们连接到提供视觉表示和命中测试的任意 UPrimitiveComponent。因此,如果你愿意,可以使用任何 UStaticMesh。默认情况下,UTransformGizmo 生成自定义 Gizmo 特定的 UPrimitiveComponents,在线条的情况下,它是一个UGizmoArrowComponent。这些 GizmoComponents 提供了一些细节,如恒定的屏幕空间尺寸、悬停支持等。但是你绝对不必使用它们,并且 Gizmo 外观可以完全根据你的目的进行定制(未来以 Gizmo 为重点的文章的主题!)。

因此,UAxisPositionGizmo 实际上只是“根据鼠标输入沿线指定位置”这一抽象概念的实现。3D 线、线位置到抽象参数的映射(默认情况下为 3D 世界位置)以及状态变化信息都通过 UInterfaces 实现,因此可以根据需要进行自定义。视觉表示只是为了通知用户,并为捕获鼠标的 InputBehavior 提供命中目标。这允许以最小的难度集成任意捕捉或参数约束等功能。

但是,这都是旁白。实际上,要使用 UTransformGizmo,你只需使用以下调用之一从 GizmoManager 请求一个:

class UInteractiveGizmoManager 

    // ... 
    UTransformGizmo* Create3AxisTransformGizmo(void* Owner);
    UTransformGizmo* CreateCustomTransformGizmo(ETransformGizmoSubElements Elements, void* Owner);

然后创建一个UTransformProxy实例并将其设置为 Gizmo 的目标。Gizmo 现在将具有完整功能,你可以在 3D 场景中移动它,并通过 UTransformProxy::OnTransformChanged 委托响应变换更改。可以使用各种其他委托,例如开始/结束转换交互。基于这些委托,你可以变换场景中的对象、更新对象的参数等。

稍微复杂一点的用法是,如果你希望 UTransformProxy 直接移动一个或多个 UPrimitiveComponent,即实现几乎每个 3D 设计应用程序都有的普通“选择对象并使用 gizmo 移动它们”类型的界面。在这种情况下,可以将组件添加为代理的目标。Gizmo 仍然作用于 UTransformProxy,并且 Proxy 将单个变换重新映射到对象集上的相对变换。

UTransformGizmo 不必为工具所有。在 ToolsFrameworkDemo 中,USceneObjectTransformInteraction类监视运行时对象场景中的选择变化,如果存在活动选择,则生成合适的新 UTransformGizmo。代码只有几行:

TransformProxy = NewObject<UTransformProxy>(this);
for (URuntimeMeshSceneObject* SceneObject : SelectedObjects)

    TransformProxy->AddComponent(SO->GetMeshComponent());


TransformGizmo = GizmoManager->CreateCustomTransformGizmo(ETransformGizmoSubElements::TranslateRotateUniformScale, this);
TransformGizmo->SetActiveTarget(TransformProxy);

在这种情况下,我将传递ETransformGizmoSubElements::TranslateRotateUniformScale以创建没有非均匀缩放子元素的 TRS gizmo。要销毁 Gizmo,代码只需调用 DestroyAllGizmosByOwner,传递创建期间使用的相同 void* 指针:

GizmoManager->DestroyAllGizmosByOwner(this);

UTransformGizmo 自动发出必要的撤消/重做信息,这将在下面进一步讨论。因此,只要使用中的 ITF 后端支持撤消/重做,Gizmo 转换也将支持。

8.2 本地与全球坐标系

UTransformGizmo 支持局部和全局坐标系。默认情况下,它从 ITF 后端请求当前的本地/全局设置。在 UE 编辑器中,其控制方式与默认 UE 编辑器 Gizmo 相同,方法是在主视口顶部使用相同的世界/本地切换。你也可以覆盖此行为,请参阅 UTransformGizmoBuilder 标头中的注释。

一个警告,不过。UE4 仅支持组件的局部坐标系中的非均匀缩放变换。这是因为在大多数情况下,不能将具有非均匀缩放的两个单独的 FTransform 组合成一个 FTransform。因此,在全局模式下,TRS Gizmo 将不会显示非均匀缩放手柄(轴括号和外角 V 形)。默认的 UE 编辑器 Gizmo 具有相同的限制,但通过仅允许在缩放 Gizmo 中使用本地坐标系(不与平移和旋转 Gizmo 组合)来处理它。

9、工具上下文和 ToolContext API

在这一点上,我们有 Tools 和 ToolManager,还有 Gizmos 和 GizmoManager,但谁来管理 Manager?为什么,当然是上下文。UInteractiveToolsContext是交互工具框架的最顶层。它本质上是工具和 Gizmo 所在的“宇宙”,并且还拥有 InputRouter。默认情况下,你可以简单地使用此类,这就是我在 ToolsFrameworkDemo 中所做的。在 ITF 的 UE 编辑器使用中,有一些子类可以调解 ITF 和更高级别的编辑器构造(如 FEdMode)之间的通信(例如,参见UEdModeInteractiveToolsContext)。

ToolsContext 还为 Managers 和 InputRouter 提供了各种 API 的实现,这些 API 提供了“类似编辑器”的功能。这些 API 的目的本质上是提供“编辑器”的抽象,这使我们能够防止 ITF 具有显式的虚幻编辑器依赖项。在上面的文字中,我多次提到“ITF 后端”——这就是我所指的。

如果仍然不清楚我所说的“编辑器的抽象”是什么意思,也许可以举个例子。我还没有提到任何关于对象选择的内容。这是因为选定对象的概念在很大程度上超出了 ITF 的范围。当 ToolManager 去构造一个新工具时,它会传递一个选定的 Actor 和组件的列表。但是它通过询问工具上下文来获得这个列表。而且工具上下文也不知道。工具上下文需要通过IToolsContextQueriesAPI询问创建它的应用程序。这个周围的应用程序必须创建 IToolsContextQueriesAPI 的实现并将其

以上是关于UE4运行时交互工具框架的主要内容,如果未能解决你的问题,请参考以下文章

UE4的学习路线,自己个人能够开发一个完整的游戏的学习路线。零基础。

交互式地图上的工具提示

(可能不是原创)Redhat 提供 Istio 在线交互式教学

Flutter 与内存变化的交互

UE4 appBitsCpy函数作用详解

UE4在运行时导入动画