引擎版本 4.22.0
属性类型定制简介
在介绍属性类型定制前先了解下引擎提供的默认的详细布局。
- 默认详细布局
只要我们的UPROEPRTY类型是值类型,编辑器系统就会在详细面板为你创建一个默认布局。如下图所示:
- 属性类型定制(Property Type Customization)
当你创建了需要更改布局的自定义结构时,属性类型定制就变得非常有用。目前属性类型定制只适用于USTRUCT的接口,IPropertyTypeCustomization实现的所有功能都只与结构体一起工作。
属性类型定制最常见的用法时创建比默认显示更好的布局,或者创建设置数据的更直观的小部件。比如引擎默认提供的通过颜色拾取器来设置颜色,软对象选择设置等。
快速创建属性类型定制
- 创建需要定制化显示的结构体
为保证在定制化代码中可以轻松访问该结构体中相关属性,该结构使用USTRUCT宏标记,成员变量使用UPROPERTY宏标记。
这里先记录下未定制化时在详细面板中的效果:
- 创建属性类型定制化类
类名请确保与自定义结构体匹配,并在末尾加上“Customization”。
该类需要继承自IPropertyTypeCustomization接口并实现接口的方法。
我们注意到IPropertyTypeCustomization接口中主要包括两个方法:CustomizeHeader和CustomizeChildren。我们不妨看看源码中的注释:
可以看出CustomizeHeader主要用来处理属性的头,而属性头是该属性在详细面板中显示的所在的行,且不能额外添加行;而CustomizeChildren主要用来处理属性中需要定制化显示的子属性及添加额外的行。一般情况下对于需要定制化显示的结构体属性中只有一个属性时可以放在CustomizeHeader中处理,而一旦该结构体中的属性有多个需要定制化显示则最好放到CustomChildren中处理。
- 注册属性类型定制化
仅仅创建属性类型定制化时不够的。为了让编辑器知道它的存在,你必须在正确的模块中注册它。由于属性类型定制化属于编辑器环境下使用的,最好将其注册在你游戏的编辑器模块。假如你的游戏中只有一个游戏主模块,你又不想添加一个专门的编辑器模块,那么你可以将其注册在游戏主模块中,但需要使用WITH_EDITOR宏标记。
这里给出一个示例,示例中将属性类型定制化类注册到专门的编辑器模块启动函数中:
当然同样需要在专门的编辑器模块的Build.cs中私有依赖PropertyEditor模块:
属性类型定制相关实验
前边介绍了创建属性类型定制的简单步骤,其中IPropertyTypeCustomization中两个方法可能还有点不太明白。这里将在前边的基础上结合一些实验来介绍定制化显示的一些操作。
- CustomizeChildren和CustomizeHeader函数中均不填写任何代码
可以看到在定义的结构体蓝图中是无法看到MyTestStruct相关属性的,很显然需要补充代码啦。
- 仅在CustomizeHeader中填写代码
在该方法中主要需要处理的是设置HeaderRow中的内容(该属性在详细面板中显示的内容),HeaderRow包含两部分:NameContent和ValueContent。如下图所示的默认bool在详细面板中分割线左侧中显示的属性名字(红框内)是在NameContent中来处理的,分割线右侧属性的值(蓝框内)是中ValueContent来处理的:
假设我们只处理NameContent部分,代码及效果下图所示:
可见只显示属性名TestStruct,特别地如果想要改名显示的属性名则使用如下代码:
由于我们的结构体包含两个变量,如果全部放到CustomizeHeader函数中处理的话则只能在ValueContent中显示了。
这里通过GetChildHandle方法分别得到结构体中两个变量Name和Table,接着使用默认的SProperty来绘制,效果如下图所示:
如果我们想要隐藏变量Table和Name的名字则只需要将代码中注释掉的.ShouldDisplayName(false)开启:
这里是为了省事直接通过SProperty来绘制,所以显示的效果很一般是默认属性显示。如果需要自己个性化处理则同样需要写Slate代码来处理,不过需要在一些事件中设置属性值或者获取属性值等,常用的方法是PropertyHandle中的SetValue和GetValue,这里就不再介绍,可以自行查看后边推荐的一些源码。
- 仅在CustomizeChildren中填写代码
常用的方法是StructBuilder.AddCustomRow,也就是添加一个自定义内容作为结构体的孩子。
效果如下:
可以看到跟前边仅在CustomizeHeader中显示效果一致。但CustomizeHeader中只能添加一行,这里可以添加多行。比如接下来将结构体中变量Name和Table分两行显示:
效果如下图所示:
- CustomizeHeader和CustomizeChildren均填写代码
保持前边CutomizeChildren最后的代码不变,在CustomizeHeader中添加如下代码:
效果如下图所示:
此时可以看到跟不做定制化处理的默认显示效果差不多。
由此也可看出CustomizeHeader和CustomizeChildren均填写代码的情况下将产生折叠效果,即CustomizeHeader中充当结构体名字显示的任务,CustomizeChildren中显示结构体成员显示的任务。至于选择在哪个函数中填写逻辑根据个人喜好即可,不必太过纠结。
- 区分是否在结构体定义蓝图中
前边的实验无一例外都是在结构体定义蓝图中的详细面板中显示,但我们还可以将自定义结构体应用到蓝图变量定义中或者DataTable中,如下图所示:
此时如果想在结构体定义蓝图中和其他环境中做不同的显示效果就需要做进一步的区分。
目前暂时没有好的办法,有一个不太靠谱的方法,代码如下:
那么对于在结构体定义蓝图中可以判断该值是否是Default Values;作为蓝图变量则判断是否是Default Value;DataTable中则判断是否是RowValue。大家如果有更好的区分方法请告知哈。
巧用属性类型定制
前边介绍了属性类型及其使用方法,也许有人会说感觉没有太大用途,无非是在详细面板中对一个结构体进行定制化显示而已。笔者近期需要做一个DataTable中引用表的一个功能,深深体会到属性类型定制的妙用。
不妨介绍下相关需求:
我们一般做DataTable需要定义一个表结构,针对手游项目的话考虑到热更新,表结构一般使用蓝图来定义。假设我有一张奖励表中需要有一个字段类型是int,填写的是物品表中的物品ID(行名)。那么我就希望在定义奖励表结构时可以制定要引用哪张表如物品表,在对奖励表中物品ID字段编辑时希望通过一个下拉菜单来显示物品表中所有的物品ID,这样不仅方便还保证不会填错物品ID。
此时要实现此功能就只需要定义一个自定义结构体包含两个成员变量int32 ID和TSoftObjectPtr<UDataTable> Table。在奖励表结构蓝图定义中添加字段时选择自定义结构体,同时在该蓝图中仅显示选择表的变量Table,而在奖励表的DataTable中仅显示一个下拉菜单来设置自定义结构体中变量ID的值。
不妨看下效果图:
注意这里为了同时显示物品表ID对应的名字,就在结构体中额外添加了一个引用的表中说明性值的列名。
可参考的相关源码
我们对于属性类型定制实际操作时可能需要一些参考源代码,这里给出相关代码大家可以自行参阅。
Engine/Source/Editor/DetailCustomizations该模块中全部时详细定制相关,其中不仅包含了属性类型定制也包含类定制,我们如果需要类定制也可参考其中的代码。
特别地值得一看的是DataTableCustomization.cpp,SoftObjectPathCustomization.cpp