2017年8月16日

使用 UMG 创建数据驱动型 UI

作者 Cody Albert

在实现游戏 UI 时,我们通常不太会考虑可维护性。我们从美工那里收到了模型,细分成多个独立元素,在屏幕上用占位符放置小部件,然后在准备好之后套入最终资产。这非常适合于 HUD 或者不需要很多迭代的菜单等元素,但如果要展示更复杂的系统该怎么办呢?我们不需要在每次添加新项目或者需要新菜单选项的时候都要不停地修改 UI,可以建立一个数字驱动型 UI,把我们从管道中解放出来,自动地将底层数据直接连接到界面。
UnrealEngine%2Fblog%2Fcreating-a-data-driven-ui-with-umg%2FDynamicUI_Fortnite_770-770x480-38b0abba26d654c36e5a4c02ebcb9314533cd6d8

数字驱动型 UI

数字驱动型 UI 元素是指根据某些底层数据源在过程中逐渐构造而成的元素,而不是手动构建。这种模式的美妙之处在于,设计师可以更改 UI 展示的系统,而不必对 UI 本身进行任何调整。最大的缺点是,由于这个元素仅在运行时存在,所以很难预览和精细地控制在游戏中显示的样子。

例如,想象一个游戏中的商店。我们界面需要展示一系列可以购买的物品,标有价格和图标。构建一个橱窗和所有这些信息并不难,但是如果设计师想要在商店中增加或者减少一些物品该怎么办?如果需要调整价格或者需要更新图标又该如何?所有这些操作都需要修改界面,如果忘了修改界面,就会导致界面和数据不同步。没有人想在游戏中购买一个标价 500 金币的物品,却发现财产清单中扣掉了 1000 金币!
在本文中,我将描述如何建立数据表,将它连接到商店小部件,这个小部件会显示滚动列表中任意数量的物品。我还将演示如何在做出选择后广播事件,稍微讲解一下如何根据这些想法进行扩展来满足项目需要。

入门

在任何数据驱动型 UI 元素中,最重要的一部分都是数据本身,所以我们先来建立一个数据表,用来包含所有商店库存物品。首先,需要创建一个结构来表示每一个表行将要包含的列。单击“新增”,打开“蓝图”类别,单击“结构”来创造结构。

我已经建立好了自己的结构,参见下图:
UnrealEngine%2Fblog%2Fstatic-analysis-as-part-of-the-process-copy%2FDataDriven1-750x105-63d44c3d94c993eedee52f540a7df3b41e1e0eda
请注意,我没有创建“名称”字段,因为这个数据表之后会自动添加这一信息。如果我们需要在表中查找具体条目,就会用到名称。

接下来,再次单击“新增”,展开其他类别来创建数据表。选择刚才创建的结构,然后开始添加一些条目吧!
UnrealEngine%2Fblog%2Fstatic-analysis-as-part-of-the-process-copy%2FDataDriven2-379x232-261fcfc757bda6129374dbe2eb29611e859744ae
现在已经有了数据,就需要创建两个小部件。首先,需要一个主要的商店橱窗,有了商店之后就会创建橱窗。然后,创造一个 ItemRow 小部件,这个会在运行时创建,用于表示数据表中的一行。

先从 ItemRow 入手吧。我们的目标是创造一个可以自动复制和填充的通用小部件,并且它的布局可以根据不同的文本长度和我们的需要进行调整。

我假设了最大文本长度和图标尺寸,以下是我的小部件:
UnrealEngine%2Fblog%2Fstatic-analysis-as-part-of-the-process-copy%2FDataDriven3-750x267-e111de989c968bab6c7ef4151e1adf264cbce800
请注意,我删除了层次结构顶部的起始画布面板,因为这个小部件将由它的父代决定位置和大小。

我还创建了 SetValues 函数,用于简化向模板供给数据的过程。我本来可以使用属性绑定,这样又快速又简单,但是绑定在每次运行时都会更新值。在可能需要担心性能的任何情况之下,最好避免使用绑定,而是建立事件以仅在需要时更新属性。
UnrealEngine%2Fblog%2Fstatic-analysis-as-part-of-the-process-copy%2FDataDriven4-750x162-93d460524beece671d753d1f0392f0e5920ac5fd
下一步是创建将用于保存条目的主要小部件。这个小部件将会包含用于读取数据表和为每一行创建 ItemRow 小部件的蓝图。我们暂时可以拖进一些 ItemRow 小部件来显示出来,但由于这些行是由蓝图添加的,所以最终成品中需要一个空的滚动框。
UnrealEngine%2Fblog%2Fstatic-analysis-as-part-of-the-process-copy%2FDataDriven5-750x410-78fd40a32f7d2322cfceead631967be6ad5278c6UnrealEngine%2Fblog%2Fstatic-analysis-as-part-of-the-process-copy%2FDataDriven6-750x138-f7777971604338d1427b71ffcebc27bd02e97dab
现在,我们只需要将主要小部件添加到视口,数据表中的所有最新数据就会自动填充到这里!如果想要添加、删除或更改任何条目,只需要更新数据表就可以了。
UnrealEngine%2Fblog%2Fstatic-analysis-as-part-of-the-process-copy%2FDataDriven7-750x693-9f5d94a0bd2994a87205c191c88a18d05168a71a

改进

你可能会注意到的第一个烦恼是必须启动 PIE 才能查看商店界面,意味着进行调整可能只是试验性的,并且会出错。如果你使用的是 4.15 或后续版本就比较幸运,因为我们为小部件蓝图添加了 Event Pre Construct 节点。本质上与 Construct 节点是一样的,唯一的区别是它是在编辑时运行的,所以不用单击播放就可以在预览视口中看到小部件的样子。只需要把用来构建行的蓝图与 Construct 节点的连接断开,然后将它连接到 Event Pre Construct 节点即可。

请注意,如果修改数据表,需要单击“编译”来重新构建预览小部件,即使这个按钮仍然显示平常的复选标记也是如此。

还需要记住的是,尽管连接到 Event Pre Construct 的节点仍会像以前一样在运行时调用,但现在也会在蓝图编辑器里面的预览小部件上调用。你还需要使用一些装饰性代码,避免引用在运行时才会存在的内容。
UnrealEngine%2Fblog%2Fstatic-analysis-as-part-of-the-process-copy%2FDataDriven8-448x385-d167cd1eb3372b4815bf7907e3bed63cb6e2d690
另一个需要注意的事项是即使目前显示的是任意数量的物品,也需要一种方法,将单击按钮事件通知给其他游戏系统(例如角色控制),这样就可以扣除金币,把物品添加到玩家的物品清单中。为了尽量保持模块化,我倾向于在顶级小部件上创建一个事件分派程序,充当各个条目和想要在按下特定按钮时接收事件的任何人之间的媒介。ItemRow 将需要一个指回根小部件的句柄,这样才能触发事件,通过覆盖 ItemRow 的 OnMouseButtonDown 可以调用该事件。
UnrealEngine%2Fblog%2Fstatic-analysis-as-part-of-the-process-copy%2FDataDriven9-1117x289-21ea993e6cd9611cbfdbc9c7c8fce7c071dfb511
在 ItemRow 小部件中。

任何相关方都可以将自定义事件绑定到根小部件的事件分派程序,以被单击物品的名称作为参数,在数据表中执行自己的查找来获取完整的详细信息。
UnrealEngine%2Fblog%2Fstatic-analysis-as-part-of-the-process-copy%2FDataDriven10-750x300-1d1f174e2d03a7a0d07e96abfd67540c9b43ff9b
在主要顶级小部件中。
还必须理解的是,我在本例中使用的数据表只是用于在编辑时进行修改。在很多情况下需要更动态的数据结构,可以在游戏过程中进行修改。甚至需要使用一个数据表作为数据的起始状态,将其复制到可以根据需要进行修改的另一个结构当中。为了尽量能够通用,可以创建一个包含任意属性和一些用于告知 UI 如何显示这些属性的元数据的数据结构。事先可以花些时间来决定一种最适合自己的解决方案,而不是鲁莽行事,在过程中各种碰壁。

最后的感想

虽然某些 UI 元素的更改并不足以保证需要额外的时间和精力来建立数据驱动型小部件,但许多系统都得益于它们所提供的灵活性。快速迭代是优秀游戏设计的核心,在过程中生成界面可以让你瞬间从想法落实到执行。