在UE引擎中有大量的配置使用ini来进行设置与控制。对于项目而言,了解其中哪些是能够更新的,能够对制定项目的更新内容规则有帮助。并且,UE很多功能都是通过CVars等方式实现的配置化和动态开关以及对指定平台或设备的Device Profiles设置,同样可以在热更中实现运行中配置的动态下发及应用。
本篇文章从引擎机制分析ini config的加载流程、以及不同的配置模块在热更之后重载、在项目中如何应用等内容,基于Ini配置文件的热更实现运行时引擎或项目中参数的修改、reapply,增强游戏的更新能力。
在研究怎么更新Ini之前,首先要分析引擎的Ini是怎么加载和被应用到游戏中的。之前我写过一篇文章分析了GConfig的加载机制:UE代码分析:GConfig的加载。
默认情况下,在引擎启动时就加载了Ini文件,它是第一批被加载的文件之一,后续引擎模块的启动都依赖于这些配置,官方的文档:Configuration Files。
引擎对于Module的加载顺序也有加载Config完毕的阶段PostConfigInit
用于保证在配置文件加载之后才启动,避免配置文件读取不到的情况:
1 | // Phase at which this module should be loaded during startup. |
引擎中启动的代码:
1 | bool FEngineLoop::AppInit() |
在FEngineLoop::AppInit
函数的末尾,执行了FCoreDelegates::OnInit.Broadcast();
这个Delegate在FCoreUObjectModule
的StartupModule中,被InitUObject()
绑定,用来初始化引擎中的UObject。在这个阶段Config也都已经加载完毕,创建UObject也可以正确地从Config中读取数据。
UE代码分析:GConfig的加载文章中介绍了FConfigCacheIni::InitializeConfigSystem
加载ini文件的过程,此处不再赘述,可以理解为执行完毕之后Config就处于可用状态了。
Ini的文件层次与优先级
更新UE的Ini需要注意的以下几个方面:
- UE中同一类别的配置,是由多个Ini文件层次结构共同组成的
- 不同文件层次的Ini具有不同的优先级,高优先级的会覆盖低优先级的值。
以Engine配置为例,引擎中可以进行配置的文件有28个,其他的类别配置如Game/Input/DeviceProfiles等也相同(UE4.25):
这些文件(如果存在)都是能被加载的,并且按照优先级升序排列。其效果就是,在PROJECT_DIR/Config/DeaultEngine.ini
中就能替换ENGINE_DIR/Config/BaseEngine.ini
中的选项。
在一般项目开发中,默认情况下工程内的属性配置一般在DefaultEngine.ini
,对某个平台的配置则是在Config/Windows/WindowsEngine.ini
中,很少会存在这么多配置文件的情况,也会造成一定程度的混乱。
所以,在更新时,可以直接使用Config/Default*.ini
和Config/PLATFORM/PLATFORM*.ini
这种方式更新。也可以使用原先不存在的文件进行更新,如原本项目中不存在Config/UserEngine.ini
,在更新时可以将需要修改的配置写入这个文件,在运行时读取追加到引擎中,在自动挂载的方式中也支持,因为Config/User*.ini
的优先级是最高的。
所以,为了实现Ini的更新,需要实现:
- 在更新时打包指定的ini文件
- 能够根据更新信息获取到引擎中已加载的FConfigFile实例,将新的配置追加其中。
- 更新使用新配置的UObject对象中的数据
UObject加载Config
在处理更新之前,需要先了解UObject加载Config的过程(只有标记了Config
的类的CDO才会执行加载),其栈如下:
大致流程为:
- 检查UClass是否具有
CLASS_Config
的Flag - 检查Property是否具有
CPF_Config
的Flag - 从字符串序列化CDO的属性
如:
1 | UCLASS(config=GameUserSettings) |
这个类的UCLASS中加了Config
,会通过UHT给它的UClass生成CLASS_Config
的Flag,并且这个类的ival
这个数据成员,也在UPROPERTY
中添加了Config
,会给当前属性的FProperty
增加CPF_Config
的Flag,这样实现代码与加载时的统一。
具体的加载实现详见CoreUObject\Private\UObject\Obj.cpp
中的LoadConfig
函数。
在创建非CDO的对象时,会从CDO中拷贝所有的属性(在构造函数执行之后):
1 | // Binary initialize object properties to zero or defaults. |
所以,UObject的对象使用的Config标记对游戏使用的影响分了两个步骤:
- 引擎启动时加载ini,创建出CDO并从ini中读取配置
- 在创建Non-CDO对象时,从CDO拷贝数据
如果想要覆盖ini中的配置值,不能直接写到构造函数里,因为LoadConfig的时机晚于构造函数。但可以重载PostInitProperties
函数,它会在LoadConfig之后调用:
1 | virtual void PostInitProperties()override; |
更新UObject中的配置值
对于热更新的需求而言,对于UObject使用的Config,我们可以在创建对象之前介入这个过程,修改CDO,在CDO创建之后也可以对Object的对象调用ReloadConfig实现从文件重新加载。
1 | bool UFlibUGConfigReloadHelper::ReloadIniFile(const FString& StrippedName,const FString& File) |
如代码所示,首先获取引擎中所有具有Config标记的UClass,并且需要Class所绑定的ini名字与需要重新加载的ini匹配,再遍历它们每个UClass的UObject实例,对其调用ReloadConfig。
注意,上述代码中先修改了对应的
FConfigFile
实例,因为在引擎启动时已经加载了旧的ini文件,存储在FConfigFile
中,在调用ReloadConfig前,需要先对他们进行更新,这样在ReloadConfig中就能从GConfig读取新的数据。
ConsoleVariables
在UE中可以通过使用ConsoleVariables
作为模块间的数据传递方式,我之前的笔记中有在运行时获取和修改Console Variables的方式:设置 ConsoleVariables 值的几种方式。
Console Variables在引擎启动时的自动应用的值有以下两种应用方法:
- 可以在
Engine/Config/ConsoleVariables.ini
中的[Startup]
中设置:
1 | [Startup] |
- 在Engine类别Ini层次中指定(如
DefaultEngine.ini
),[ConsoleVariables]
:
1 | [ConsoleVariables] |
当我们对Engine类别的FConfigCache更新之后,调用以下函数:
1 | ::ApplyCVarSettingsFromIni(TEXT("ConsoleVariables"), *GEngineIni, ECVF_SetBySystemSettingsIni); |
应用[ConsoleVariables]
中最新的CVars值。如果想要同时更新ConsoleVariable.ini
中的[Startup]
和[ConsoleVariables]
中的设置,可以调用:
1 | FConfigCacheIni::LoadConsoleVariablesFromINI() |
它会先Apply[Startup]
中的,再Apply[ConsoleVariables]
中的。
更新的流程步骤:
- 重新加载Engine类别的Ini文件
- 调用ApplyCVarSettingsFromIni函数。
Device Profiles
Device Profiles是UE为特定平台或设备设定参数的一种方法,官方文档:Setting Device Profiles。可以通过它来实现不同平台、设备间的性能、参数适配等需求。
引擎中的默认提供的平台、设备名与Ini平台名的配置:BaseDeviceProfiles.ini#L6,存储在*DeviceProfiles.ini
的[DeviceProfiles]
下DeviceProfileNameAndTypes
的值。在BaseDeviceProfiles.ini中具Profile的依赖配置、设备指定、IOS设备映射、指定设备的配置等等,自己项目中的设置可以参考。
并且,UE的Device Profiles是支持继承的,如iPhoneX->IOS_High->IOS->Mobile,能够方便地复用配置。
UDeviceProfile
类也是标记为Config
的,并且是perObjectConfig
,每个对象的配置都会单独保存和加载。简而言之,Deviece Profiles的加载,其实也就是创建了很多个的对象,每个对象会保存自己平台或设备的配置信息,NewObject<UDeviceProfile>
传递的Name就是平台名。
在创建UDeviceProfile时会自动从Ini中加载(NewObject<UDeviceProfile>
的栈):
既然Device Profiles的配置加载也是走的默认的LoadConfig流程,那么我们对UDeviceProfile的更新就可以按照更新UObject中所使用的配置值该节的步骤,获取所有的UDeviceProfile实例,对其调用ReloadConfig,并且需要对其的Parent Profile也调用(Parent Profile也是UDeviceProfile对象):
1 | void UFlibUGConfigReloadHelper::RecursiveReloadDeviceProfile(UDeviceProfile* Profile) |
之后可以调用UDeviceProfileManager
的ReapplyDeviceProfile
:
1 | UDeviceProfileManager::Get().ReapplyDeviceProfile(); |
GameUserSettings
在重载GameUserSettings
的配置之后,可以调用ApplySettings
进行应用:
1 | void UFlibUGConfigReloadHelper::ReapplyGameUserSettings() |
ApplySettings
的代码为:
1 | void UGameUserSettings::ApplySettings(bool bCheckForCommandLineOverrides) |
结语
UE中对于各种配置文件的加载其实都是基于UE的Ini层次的更新,首先需要更新GConfig中的值,再将其应用到不同的模块,文章中列举了数种不同配置的重载和应用,其他未列举的情况也可参照相同的方式进行处理。