理解UDynamicSubsystem
顾名思义,其为动态的Subsystem,会被动态对待。这里的动态特指随着模块的加载释放来创建和销毁。
为什么要如此设计?
首先要先理解UE的模块(Module)机制:
- 一个uproject项目或uplugin插件可以包含多个Module。
- 每个Module有自己的Build.cs。
- 每个Module可以被编译成dll。
- Module之间可以相互引用。
因此一个Module可能会有多个依赖的其他Modle,我们姑且称被依赖的Module为DependencyModules。
UDynamicSubsystem的创建与销毁
假设
- 我有一个插件(Plugin)名为MyPlugin.uplugin,一个项目名为Hello.uproject。
- Hello.uproject想使用MyPlugin.uplugin插件。
- MyPlugin.uplugin里面依次定义了UMyPluginEngineSubsystem,UMyPluginEditorSubsystem和UMyPluginGameInstanceSubsystem。
我们可能有两种引用Module的情况,一种为显式添加Plugin / DependencyModules(即事先配置好),另一种为动态加载Plugin / DependencyModules。
显示添加Plugin / DependencyModules
配置Plugin / DependencyModules
- 在Hello项目的插件设置上开启MyPlugin。
- 或在Hello.uproject文件中手动添加
"Plugins": [{"Name": "MyPlugin","Enabled": true}]
。 - 或在Build.cs中添加
PublicDependencyModuleNames.Add("MyOtherModule")
。
这样当Hello模块启动时,引擎会自动加载其依赖模块(MyPlugin)。UMyEngineSubsystem和UMyPluginSubsystem在编辑器启动时就会创建并Initialize。
Initialize函数的部分实现(点击展开/折叠代码块)
void FSubsystemCollectionBase::Initialize(UObject* NewOuter)
{
// 如果是UDynamicSubsystem的子类
if (BaseType->IsChildOf(UDynamicSubsystem::StaticClass()))
{
for (const TPair<FName, TArray<TSubclassOf<UDynamicSubsystem>>>& SubsystemClasses : DynamicSystemModuleMap)
{
for (const TSubclassOf<UDynamicSubsystem>& SubsystemClass : SubsystemClasses.Value)
{
if (SubsystemClass->IsChildOf(BaseType))
{
AddAndInitializeSubsystem(SubsystemClass);
}
}
}
}
else
{ // 普通Subsystem对象的创建
}
}
引擎除了加载了自定义的DynamicSubsystem,也加载了引擎自身实现的DynamicSubsystem,这些DynamicSubsystem被存储在DynamicSystemModuleMap(UE5源码中为GlobalDynamicSystemModuleMap)中,其中最重要的就是UnrealEd模块。
UnrealEd模块本身定义了几个Subsystem:
- AssetEditorSubsystem:资产编辑器子系统。
- BrushEditingSubsystem
- ImportSubsystem:用于在编辑器中导入资源的子系统,包含用hooking导入的实用函数和回调。
- LayersSubsystem
- PanelExtensionSubsystem:用于在编辑器中创建可扩展面板的子系统。
那MyPlugin里的Subsystem什么时候创建呢?
其实在第一次初始化时,会调用FSubsystemModuleWatcher的InitializeModuleWatcher函数。
InitializeModuleWatcher函数的部分实现(点击展开/折叠代码块)
void FSubsystemCollectionBase::Initialize(UObject* NewOuter)
{
// ...
// 静态变量,用数目来判断是否为第一次创建
if (SubsystemCollections.Num() == 0)
{
FSubsystemModuleWatcher::InitializeModuleWatcher();
}
// ...
}
void FSubsystemModuleWatcher::InitializeModuleWatcher()
{
// ...省略一些check()
// 获得所有UDynamicSubsystem的子类
TArray<UClass*> SubsystemClasses;
GetDerivedClasses(UDynamicSubsystem::StaticClass(), SubsystemClasses, true);
for (UClass* SubsystemClass : SubsystemClasses) //遍历
{
// 检查是否有CLASS_Abstract抽象类标记
if (!SubsystemClass->HasAllClassFlags(CLASS_Abstract)) //不为抽象类
{
// 获得所属于的包
UPackage* const ClassPackage = SubsystemClass->GetOuterUPackage();
if (ClassPackage)
{
const FName ModuleName = FPackageName::GetShortFName(ClassPackage->GetFName());
if (FModuleManager::Get().IsModuleLoaded(ModuleName))
{
TArray<TSubclassOf<UDynamicSubsystem>>& ModuleSubsystemClasses = FSubsystemCollectionBase::DynamicSystemModuleMap.FindOrAdd(ModuleName);
// 添加到DynamicSystemModuleMap
ModuleSubsystemClasses.Add(SubsystemClass);
}
}
}
}
// 注册模块加载和释放事件
ModulesChangedHandle = FModuleManager::Get().OnModulesChanged().AddStatic(&FSubsystemModuleWatcher::OnModulesChanged);
}
在上述代码块中,首先是遍历了当前进程里的UDynamicSubsystem子类,并按照模块划分存储进DynamicSystemModuleMap。这样之后就知道当加载或销毁某个模块时应该加载和销毁哪些Subsystem。
其次是OnModuleChange事件的注册,这样就可以能够在模块加载/卸载的时候加载/卸载其包含的DynamicSubsystem
OnModuleChange函数的实现(点击展开/折叠代码块)
void FSubsystemModuleWatcher::OnModulesChanged(FName ModuleThatChanged, EModuleChangeReason ReasonForChange)
{
switch (ReasonForChange)
{
case EModuleChangeReason::ModuleLoaded:
// 创建一个模块的DynamicSubsystem类们
// 查找该模块里定义的类看看是否是UDynamicSubsystem子类,然后为其创建对象实例。
AddClassesForModule(ModuleThatChanged);
break;
case EModuleChangeReason::ModuleUnloaded:
// 销毁一个模块的DynamicSubsystem类们
// 销毁并移除登记
RemoveClassesForModule(ModuleThatChanged);
break;
}
}
这其中的关键就是SubsystemCollections(在UE5的源码中为GlobalSubsystemCollections)是一个静态变量,其引用了整个进程中所有定义的FSubsystemCollection的数量。这么写其实是为了做一个保险,保证每个类型的SubsystemCollections都能正确的创建多个。
总结
整个流程其实就是在MyPlugin模块被加载的时候,会自动触发OnModulesChanged事件,从而被自动的创建出内部的UMypluginEngineSubsystem和UMyEngineSubsystem。
动态加载Plugin / DependencyModules
在UE中可以使用 FModuleManager::Get().LoadModule(TEXT("MyOtherModule"))
动态加载Module,如果这个模块里有定义DynamicSubsystem其会被创建出来。
对于Plugin必须要在MyPlugin.uplugin里面加上 "ExplicitlyLoaded": true
,这一句很重要,表明后续要显示的动态加载该插件。
根据路径动态加载Plugin(点击展开/折叠代码块)
FString path= FPaths::Combine(FPaths::ProjectPluginsDir(),TEXT("MyPlugin/MyPlugin.uplugin"));
// 添加插件路径让可以找到
IPluginManager::Get().AddToPluginsList(path);
// 显式加载
IPluginManager::Get().MountExplicitlyLoadedPlugin(TEXT("MyPlugin"));
在加载Plugin / DependencyModules后,其包含的UMyPluginEngineSubsystem和UMyPluginEditorSubsystem就会在这个时候被创建出来。
总结
MyPlugin里的DynamiacSubsystem对象虽然都是靠OnModulesChanged事件来创建和销毁自己,但是根据项目配置模块引用的不同,时机也可以不同。
DynamicSubsystem会根据Module的加载和释放来创建和销毁,这就是动态的含义。
一些思考
动态加载MyPlugin里的UMyPluginGameInstanceSubsystem可以正常工作么?
只有MyPlugin已经被加载进来后,再点击Play,这个时候就可以正确的使用MyPlugin中的非DunamicSubsystem了。
为什么只有UEngineSubsystem和UEditorSubsystem才是UDynamicSubsystem?
从生命周期来考虑,只有UEngineSubsystem和UeditorSubsystem的生命周期是跟编辑器的进程绑定在一起的,且对于另外三个:UGameInstanceSubsystem,UWorldSubsystem和ULocalPlayerSubsystem其本身就是随着Play和Stop来反反复复的创建和销毁,本身已经足够动态了。
参考
《InsideUE4》GamePlay架构(十一)Subsystems:https://zhuanlan.zhihu.com/p/158717151