在我们开发UE插件时,为了灵活控制,通常会提供大量的配置化参数,用于控制插件具体的执行逻辑和行为。
本文是我UE插件开发系列文章的第八篇,将介绍我在开发插件的过程中,对于插件配置化能力的思考和实现,在项目配置、任务配置、动态参数替换等方面的实践,使插件的配置流程尽可能地足够灵活易用。
项目配置
全局配置创建
插件可以在Project Settings
-Plugins
分类下,创建独属于插件本身的配置,控制插件行为。
以HotPatcher为例,在Project Settings
-HotPatcher
下,就有创建:
在项目设置下,是要对插件进行全局控制的逻辑,也就是插件在当前引擎环境中运行的全局参数,并且配置的值是存储在Config
内的,可以提交到仓库中,作为整个项目的默认配置。
创建方式,需要先创建出一个UObject
的类:
1 | UCLASS(config = Game, defaultconfig) |
把想要暴露出的配置参数,都加上反射标记,并且UCLASS
上要加上Config相关的标识,这样编辑后的ini会存到[PROJECT_DIR]/Config
下面,不过具体存到哪个ini里去,就要根据情况自己决定了。如果是纯Editor相关的,可以放到Editor
下面去。
创建完之后,还需要把它注册到引擎中去,可以在引擎启动时,可以通过SettingsModule注册。同样以HotPatcher为例:
1 | if (ISettingsModule* SettingsModule = FModuleManager::GetModulePtr<ISettingsModule>("Settings")) |
这里的逻辑,就是把UHotPatcherSettings
这个类的CDO,绑定到了Project Settings
-Plugins
-Hot Patcher
上。
在项目设置中,点开Hot Patcher
的项,就会根据CDO
的情况列出所有的配置参数了。
配置保存
修改了其中的值之后,会触发UObject::SaveConfig
,把当前CDO的值,保存到对应的ini中去:
保存的配置:
1 | [/Script/HotPatcherEditor.HotPatcherSettings] |
它就是典型的CDO默认参数配置,在引擎启动时,创建出CDO后,会从UClass指定的ini里读取值并写入。
这样就能够实现插件在项目中通用配置的存储、修改了。
访问配置
如何使用呢?
因为我们用于保存插件配置的对象就是CDO,如果想要访问到引擎当中的实时值,就可以通过CDO的方式获取:
1 | const UHotPatcherSettings* Settings = GetDefault<UHotPatcherSettings>(); |
然后就跟普通的属性访问一样,去获取对应的值就行了。
以Hot Patcher配置中的bWhiteListCookInEditor
为例:
它控制的就是,只在Cook And Pak
中列出指定的白名单的平台列表。
通过这个方式就能为插件比较方便地做一些全局通用行为的参数控制了。
任务配置
前一小结提到,可以为插件创建出全局的配置,用于控制通用行为。
那么在此基础上,我们有时需要让插件执行一些不同的任务,这些不同的任务之间,也会使用不同的参数。所以,通常还需要为插件提供一个用于执行任务的配置化逻辑。
PropertyEditor创建
同样以HotPatcher为例,在主界面打开byPatch
,就会弹出一个配置面板:
用于编辑执行任务的配置,并且在右上角还提供了三个选项,分别是Import
/Export
/Reset
,用来方便导入、导出、和重置当前的配置。
这样可以把每一个执行任务,都独立出一份配置文件,当需要执行时,就选择对应的配置文件就可以了。
那么如何做到呢?
同样以HotPatcher为例,需要先创建出一个反射结构,并声明所有需要在配置中访问的属性:
1 | USTRUCT() |
整个执行配置的所有选项都定义在其中,在实际执行时,也是通过配置文件构造出一个该结构的实例来运行的。
创建完结构后,还需要创建出一个Slate控件,用于容纳我们的配置化界面,其实本质上就是需要把这个反射结构的跟Editor绑定起来,允许编辑。
可以使用PropertyModule
来创建出一个SettingView:
1 | void SHotPatcherPatchWidget::CreateDetailsView() |
并且把这个SettingView
与这个反射结构、实例绑定起来,从而实现Editor中修改了值,对应的内存实例也会同步修改的需要。
SettingView
创建之后,还需要把它的控件,放到指定的Slata容器控件中:
这样,当打开我们创建的Slate控件,我们所定义的反射结构(FExportPatchSettings
)的所有反射属性值,就会以引擎PropertyEditor
的形式列出来了,如同HotPatcher的配置界面那样:
通过这个配置界面修改值,就会同步修改到底层绑定的FExportPatchSettings
对象实例,我们对于配置的读取和保存,也是通过访问这个反射结构实例来做的,后面会具体介绍。
配置的序列化
前面已经提到了,通过PropertyEditor
我们可以把属性窗口跟内存中的反射结构实例绑定起来,那么对于我们想要读取/保存/重置的需求而言,只需要能够修改这个结构的实例就可以了。
如何做呢?
保存
对于配置的保存,我们可以把一个反射结构实例的值,保存为一个json结构,引擎本身也提供了这样的能力。
首先可以把一个反射结构,转换为一个JsonObject:
1 | FExportPatchSettings StructIns; |
然后就可以把这个JsonObject转换为一个Json的字符串:
1 | FString JsonString; |
拿到字符串之后,就可以存盘了,它就是我们想要存储的配置文件:
1 | FFileHelper::SaveStringToFile(JsonString,*SavePath); |
加载
当把配置文件保存之后,对于配置文件的加载,其实就分为那么两步:从json文件刷新反射结构实例的属性、更新PropertyEditor展示的值。
对于从json反序列化为UStruct,也可以通过FJsonSerializer
相关的辅助函数做到:
- 从json文件读取字符串,然后deserialize为JsonObject
- 根据JsonObject去修改反射结构实例的值
1 | FExportPatchSettings StructIns; |
这样就能够把json中的配置,覆写到实例中了。
然后还需要刷新PropertyEditor
的SettingView
,确保显示的跟实际值一致:
1 | SettingsView->GetDetailsView()->ForceRefresh(); |
重置
经过前面两部分的介绍之后,其实重置逻辑就更为简单了:只需要把反射结构实例重置为默认构造值就行了,然后刷新SettingView。
1 | StructIns = FExportPatchSettings{}; |
这样就实现了最基础的重置逻辑。不过HotPatcher做的还更复杂一些,会重置为Project Settings
中预设的配置值,但基本原理是一致的。
动态参数替换
当我们能够导出和导入配置后,不仅仅只会在Editor下使用,通常还会提供cmdlet来在ci系统中进行自动化执行。
那么对于多配置的情况而言,允许cmdlet指定配置文件来控制执行的任务,是一个不错的选择。
如HotPatcher
和ResScanner
提供的那样:
1 | -run=HotPatcher -config=config.json |
虽然比较方便,但是还存在一个痛点:如果两个要执行的任务,大多数参数都一样,只有个别的参数不一致。需要为每种情况都创建出一个独立的配置文件,那么管理起来就比较麻烦了。
所以这种情况下,就需要提供一种可以动态控制某个参数值的逻辑,让我们能为一批相似逻辑提供一个通用的配置,然后通过命令行动态修改所需的值。
基于这种需要,我写了一个模板函数,可以通过指定一个结构实例,以及指定一个Name-Value
的键值对,来实现反射属性的覆写:ReplaceProperty.hpp
只需要在cmdlet
执行参数后指定结构属性即可,并且还支持多级结构,如:
1 | -run=HotPatcher -config=config.json -bByBaseVersion=true -BaseVersion.filePath="" -versionid="0.1.00" -savePath.path="[OUTPUT_DIR]" |
首先读取config并反序列化,然后再执行参数覆写,这样就能够方便地动态控制参数了。
其实不仅仅是cmdlet的配置,项目配置中的CDO配置也可以通过参数来动态替换,与这里介绍的是类似的,就不再详细赘述了。
总结
本篇文章介绍了几种在UE中为插件添加配置化能力的方式,基于这几种方式,能够让插件的灵活性大幅提升。配置化、灵活控制永远是插件开发追求的主题,工具也需要不断地进行迭代。
除了上面介绍的这些之外,还可以对PropertyEditor进行定制化,把配置界面的展示与实际保存的数据类型脱离,比如底层结构存储的是一个字符串,但是配置界面可以特殊处理展示,就像之前的一篇文章(虚幻引擎中的属性面板定制化)里写的那样。