这是我在其他人的虚幻引擎项目中经常遇到的一种常见(反)模式:一个带有附加的一个或多个碰撞组件的网格。这虽然可行,但并非最佳实践,可能表明开发者习惯于 Unity。

Unity 与 Unreal Engine:组件组合 vs 组件内组合

标准变换层次结构

Unity 与 Unreal Engine 之间的一个重要架构差异在于组件与标准变换层次结构之间的关系。所谓“标准变换层次结构”,指的是内置组件和用户代码共享访问一组通用变换(位置、旋转、缩放)和层次结构(父子引用)值的接口。在任何现代游戏引擎中,总有一个单一的 1 核心变换层次结构实现,作为所有 3D 内容的可访问且可预测的骨干。正是这种原生层次结构使得编辑器的拖动移动功能、重叠检测以及渲染剔除等功能得以实现,而无需游戏开发者显式地在编辑器事件、物理子系统和渲染子系统之间读取和写入变换值。

在 Unity 中,Transform 组件是标准变换层次值的唯一所有者,并且是一个密封类;每个游戏对象有且仅有一个 Transform 组件,反之亦然;依赖于 Transform 的组件(例如需要知道渲染位置的网格)使用其游戏对象的唯一 Transform 组件的变换信息。

在虚幻引擎中,场景组件(Scene Component)是标准变换层次结构值的指定所有者;依赖变换的组件是场景组件的子类。虽然角色(Actor)似乎也具有标准变换值,但这仅在每个角色至少有一个场景组件的情况下成立(相反,场景组件完全可以在没有任何所属角色的情况下存在)。

基本上,区别归结如下:两个引擎都有由“组件”类组成的“实体”类。在 Unity 中,“实体”类具有一个变换(transform);而在 Unreal Engine 中,“组件”类可以具有变换。这种差异看似细微,但却具有重要的架构意义,并反映了每个引擎更广泛的设计模式。

模式推演:组合 vs 继承?

Unity 中的 Mesh Renderer 没有 transform 值,因为它使用其 Game Object 的 Transform。同样,它实际上也不拥有对其所渲染的网格的任何引用:相反,它从其相邻的 Mesh Filter 借用该值。如果 Game Object 还具有 Mesh Collider,这是一种依赖于三角网格使用的不同类型的功能,它将使用与其相邻的 Mesh Renderer 相同的 Mesh Filter。与 Transform 一样,Mesh Filter 每个 Game Object 仅限一个。

在虚幻引擎中,静态网格体组件需要一个变换值,因此它继承了场景组件。样条网格体组件需要执行与静态网格体组件相同的操作,但具有额外的细微差别,因此它继承了静态网格体组件。虚幻引擎的静态网格体组件没有将“网格渲染”和“网格碰撞”拆分为单独的组件,而是实现了渲染和碰撞(每个功能都可以选择性地激活或停用)。

这确实看起来像是组合与继承的问题。考虑到“组合优于继承”的箴言,你可能会认为 Unity 选择了更好、更灵活的方法。然而,事实恰恰相反,Unity 的方法只是将耦合转移到了一个不太方便的抽象层次:Game Object 最终被定义为“一组具有相同变换、网格资源引用、物理属性……”等的组件。

这限制了 Game Object 作为“实体”机制的实用性。一组逻辑上构成一个实体(玩家)的组件可能具有不同的位置和网格(右手、左手、摄像机)。在这种情况下,Unity 要求你将该实体分解为松散的部分实体的混合体,因为作为一个引擎,它将“实体”与“位置”、“网格”以及许多其他概念耦合在一起。

另一方面,虚幻引擎(Unreal Engine)中的 Actor 可以定义为一种机制,用于将应该作为一个整体进行生成/销毁/网络剔除的事物进行分组。

Unreal Engine 的解耦方法使您能够更清晰地通过引擎工具表示和强制执行游戏的主要逻辑封装。正如您在上面的截图和下图中所看到的,这产生了一个更精简的对象模型,使得开发者更容易编辑游戏,同时也使得 CPU 更容易运行游戏。

Unity(左)与 Unreal(右)

Unreal Engine 的组件确实表现出有时极为深远的继承层次,但一旦你“掀开引擎盖”,就会发现大量的组合结构。 FPrimitiveSceneProxy 是实际由渲染子系统解析的对象类型。 FBodyInstance 是实际由碰撞子系统解析的对象类型。 UStaticMeshComponentFPrimitiveSceneProxyFBodyInstance 组成,通过一种方式将它们包装起来,使它们协调一致地表示一个事物,你可以在编辑器中轻松定义并通过高级脚本实现。

在 Unity 中,组件是唯一一种将单个事物的物理和视觉方面组合在一起的方式,因为组件是我们开发者能够访问这些子系统的唯一途径。在虚幻引擎中,我们只是拥有更多层次的组合,其中非托管(非反射)的 C++ 类是最底层。

因此,虽然可以使用组件来组合一个物体的视觉和物理方面,但这既不是必要的,也不是理想的——引擎已经在较低层次上将这些方面组合在一起,并允许我们通过静态网格体组件的属性来微调它们的组合细节。

我提到了虚幻引擎的方法在某些方面更具可管理性,但它并非完全没有缺点。从技术上讲,每个静态网格组件附加的碰撞相关信息会带来一些内存开销,包括那些实际上不需要碰撞的组件(尽管在相同情况下,Unity 所需的额外组件和游戏对象实例的内存开销可能更大)。最显著的是,未使用的碰撞属性在编辑器中造成了视觉上的杂乱,解析起来颇为繁琐。Unity 典型的属性面板有一种令人愉悦的极简主义风格,使得在编辑器中工作时更容易聚焦于相关属性。

不要这样做

在澄清了所有这些内容之后,让我们重新审视本文开头提到的强化网格。你现在知道,静态网格组件已经将渲染和碰撞子系统组合在一起,因此额外的组件并非必要。不必要的并不总是等同于“坏”,但在这种情况下,它确实等同于“更糟”。

与仅有一个组件相比,拥有三个组件的缺点,在其他条件相同的情况下,包括

  • 内存碎片。
  • 额外使用的内存。
  • 在编辑器中定义和维护的属性增加了三倍。
    • 如果你希望它们表现一致,就必须手动维护。
    • 或者,您可以配置组件以将它们的物理体焊接在一起,这将迫使所有组件具有相同的每体设置...
      • 但随后您在详细信息面板中有一些设置实际上没有任何效果,这可能会在以后造成混淆。
      • 每次移动父对象时,引擎最终会执行更多的变换重新计算。
  • 如果组件配置为产生重叠事件,那么 Update Overlaps 函数将运行 3 次,而不仅仅是一次。Update Overlaps 以性能不佳而闻名。

对于单个实例,这可能永远不会造成明显的差异。然而,当应用于多个角色,每个角色又有多个实例,贯穿整个游戏时,最终可能会产生数百或数千个组件的差异,这可能会导致在编辑器中调整属性时花费数小时,并且在低端移动设备上每帧增加几毫秒的开销。

请这样做

我将一步步详细讲解正确的实现方法。

如果目标仅仅是赋予网格碰撞,那么只需在网格组件本身上启用所需的碰撞设置(与碰撞组件上找到的完全相同)即可。然而,如果有人在静态网格下添加碰撞组件,很可能是因为他们希望碰撞由特定形状排列组成,无论是出于功能目的还是仅仅为了优化。

这对于本示例确实如此。只需简单地将物理配置文件附加到静态网格组件上,就足以使其与物体发生碰撞,但这样做会使用三角网格碰撞。使用单个胶囊和单个盒子在计算上会更便宜,而且您可能还需要区分与手柄和物品主体的碰撞。

我一直在讨论静态网格体组件,并提到它承载了与渲染和物理相关的属性。静态网格体资源也是如此,你可以在其中找到与当前任务最相关的属性。

可以在内容浏览器中使用静态网格体组件详细信息中的“浏览到”(🔍)图标找到相关的静态网格体资产。从那里,可以像编辑其他资产一样打开和编辑它。

在虚幻引擎中,如同在 Unity 中,静态网格资源隐式地提供三角网格碰撞。在虚幻引擎中,静态网格资源还可以包含“简单碰撞”。这一特性使您能够实现特定形状的排列,而无需使用额外的组件。

打开静态网格体资源后,首先要确保“简单碰撞”可视化已开启。

打开静态网格并查看其简单碰撞时,您可能会发现某种简单碰撞已经定义(可能是导入时生成的,或者是导入时自动生成的)。在特定情况下,已经存在一个凸包,但使用胶囊 + 盒子的组合在性能上更高效,并且能实现更高保真度的碰撞行为。

碰撞菜单位于静态网格体资产编辑器的顶部工具栏中,在这里我们可以找到删除现有不需要的碰撞以及添加新形状的功能。

添加后,可以使用 3D 小部件和/或细节面板修改形状。

这些更改将立即对所有静态网格实例生效(当然,不包括那些配置为忽略简单碰撞并始终使用复杂碰撞的实例)。您可以通过在关卡编辑器中打开“显示碰撞”(alt + C)来确认这一点。

如果我想在两个地方使用相同的网格,但使用不同的碰撞形状,该怎么办?

您有几个选项:

  1. 复制网格资源。
  • 优点:组件更少 = 性能更佳。
  1. 使用其他组件代替使用静态网格组件自身的碰撞。
  • 优点:仅一份网格资源副本 = 更小的构建体积。
  1. 在同一单个网格资源上定义所有形状。在运行时选择性地激活/停用它们。
  • 优点:性能更佳,构建体积更小。
  • 缺点:需要付出努力。

如果网格复杂,选项 1 可能会比选项 2 使用更多内存。如果网格简单,选项 2 可能会比选项 1 使用更多内存。

如果我希望使用不同的形状进行重叠检测与物理碰撞怎么办?

这可以在资源中按形状进行控制:

如果我需要知道哪个形状被击中了呢?

每个形状都可以被赋予一个名称:

相关形状的名称通过 Sweep Result 结构体提供:

骨骼网格(Skeletal Meshes)呢?

对于骨骼网格(Skeletal Meshes)也是如此:可以通过将碰撞组件附加到骨骼网格组件来添加碰撞,但更好的做法是在资产编辑器中添加碰撞,这样它可以直接从单个骨骼网格组件中体现出来。概念是相同的,但由于骨骼网格碰撞需要考虑运动和其他附加因素,步骤略有不同。

UE4 与 UE5 有何不同?

UE5 在表面上看起来有很大的不同,但就本文讨论的所有内容而言,唯一的区别是 UE4 的编辑器在稍有不同的位置有一两个菜单。

综上所述

在虚幻引擎中制作碰撞时,建议在网格体资源本身上定义碰撞。这更符合虚幻引擎的架构,有助于提升性能,并且在需要在多个地方使用该碰撞时,维护起来也更加方便。