在游戏开发过程中我们往往需要创建一系列的工具来辅助我们开发,例如 UI 管理工具,各类导表工具。在 UE4.22 之前我们只能够 自己编写单例,并且自己管理生命周期。或者直接将管理游戏的工具编写进 GameInstance
中。但是随着代码量的增加,GameInstance
将会变得难以维护。在 4.22 版本发布了之后,我们可以直接将工具写在 Subsystem
中,让引擎帮我们自动管理工具类的生命周期,不再需要自己维护工具的生命周期或者修改引擎的类(如 GameInstance
)。
在 Subsystem 出现之前的黑暗时代
我们往往需要一个全局的,生命周期是在整个游戏进行的过程中一直存在的单例,而如果你想要在 UE4 里面实现一个单例,那么你需要使用以下代码:
UCLASS()
class HELLO_API UMyScoreManager : public UObject
{
GENERATED_BODY()
public:
// 一些公用的函数或者Property
UPROPERTY(EditAnywhere, BlueprintReadWrite)
float Score;
UFUNCTION(BlueprintPure, DisplayName = "MyScoreManager")
static UMyScoreManager *Instance()
{
static UMyScoreManager *instance = nullptr;
if (instance == nullptr)
{
instance = NewObject<UMyScoreManager>();
instance->AddToRoot();
}
return instance;
//return GetMutableDefault<UMyScoreManager>();
}
UFUNCTION(BlueprintCallable)
void AddScore(float delta);
};
这就对新人很不友好了(又一个不让新人碰 C++ 只让写 Lua 的原因),UE4 的实现比较难看懂,而且容易出错。例如很多人会忘记加上 instance->AddToRoot();
,如果不记得加上,那么刚刚生成的对象可能会被 GC 掉,调用的时候会导致崩溃。而且用这种方式创建的单例会在 Editor模式 下 继续存在 ,所以运行预览和停止预览之后并 不会销毁 ,下一次预览的时候里面的数据可能还是上一次运行的数据。如果想要处理这个问题,就需要自己手动加上 Initialize()
和 Deinitialize()
函数,手动调用,自己管理生命周期。
或者是另一种方法,直接把单例写进 UGameInstance
的子类里面,然后在 UGameInstance
的 Init
和Shutdown
里面进行创建和销毁。但是即便是这样也需要手动为每一个单例类写一遍,很容易出错,也不容易维护。
总而言之,不管是什么样的实现方法,UE4 客户端开发都得要自己管理好自己写的单例类的生命周期,心智负担极大。所以官方推出了 Subsystem
,并自己用在了 UE4 的部分组件的开发中(如 VaRest
,官方用 Subsystem
制作了 REST API
插件),方便引擎开发、客户端开发人员对引擎或者游戏做扩展、插件,同时不用自己操心生命周期的问题。
Subsystem 时代
为什么使用 Subsystem
用 Subsystem 的好处:
- 不需要自己管理生命周期,引擎自动帮你管理,而且保证和指定的类型(目前只有5种)生命周期一致;
- 官方提供蓝图接口,能够很方便地在蓝图调用
Subsystem
; - 与
UObject
类一样,可以定义UFUNCTION
和UPROPERTY
; - 容易使用,只需继承需要的
Subsystem
类型就能够正常使用,维护成本低; - 更模块化,而且可以迁移某个
Subsystem
到其他游戏项目使用;
所以为了代码更加方便维护与移植,还是使用 Subsystem
编写需要用到的工具比较好。
Subsystem 简介
传统美德,先附上官方的介绍:
Subsystems in Unreal Engine 4 (UE4) are automatically instanced classes with managed lifetimes. These classes provide easy to use extension points, where the programmers can get Blueprint and Python exposure right away while avoiding the complexity of modifying or overriding engine classes.
下面简单翻译一下。
UE4 会自动实例化你编写的 Subsystem,并且根据你的 Subsystem 类型(目前有5种类型)管理 Subsystem 的生命周期。Subsystem 能够暴露接口给蓝图和 Python 使用,不需要修改或者继承引擎的类(如 GameInstance )。
目前 UE4 支持的 Subsystem 类型有以下 5 种:
- Engine 类:
UEngineSubsystem
- Editor 类:
UEditorSubsystem
- GameInstance 类:
UGameInstanceSubsystem
- World 类:
UWorldSubsystem
- LocalPlayer 类:
ULocalPlayerSubsystem
名称分别对应他们依存的 Outer 对象(称之为 Outer 是因为源代码里面指向这些对象的指针名为 Outer),以及他们对应的生命周期分别是:
UEngine* GEngine
(引擎启动期间存在,或者说游戏进程期间存在);UEditorEngine* GEditor
(编辑器启动期间存在);UGameInstance* GameInstance
(游戏运行期间存在);UWorld* World
(关卡运行期间存在,一个游戏可能会有多个关卡,另外要注意的是编辑器下看到的场景其实也是一个 World);ULocalPlayer* LocalPlayer
(本地玩家存在的时候存在,实际上通常和GameInstance
生命周期差不多,但是可能有多个本地玩家,而且游戏进行过程中可以随时添加减少本地玩家,所以生命周期视情况、Outer 对象依附于哪个 LocalPlayer 而定);
他们其实之间没什么区别,默认的功能都较为相似,目前主要的区别在于不同类型的 Subsystem 生命周期不同 。所有的 Subsystem 都直接或者间接继承了 USubsystem
类。我们用张图来大致展示一下各个系统的类的关系:
图中的 UDynamicSubsystem
,前面没有提到,所以这里简单介绍下。我们可以看到图中主要是 EditorSubsystem
和 EngineSubsystem
继承了 DynamicSubsystem
,这是因为这两类 Subsystem 主要是类似模块,能够随时 加载 和 卸载 。而 DynamicSubsystem
就能提供这种功能,让这类 Subsystem 只有在需要的时候加载进入编辑器或者游戏引擎中,不需要的时候就可以卸载掉。UDynamicSubsystem
提供的额外功能只有加载和卸载功能。
其他 3 类 Subsystem 会在对应的 Outer 对象生命周期内自动创建,Outer 对象生命周期结束的时候才会被自动销毁。这 3 类 Subsystem 相比于没有继承 DynamicSubsystem
的 Subsystem 少了加载和卸载的功能,其他方面没什么区别。编写自定义的 Subsystem 的时候只需要关注 Subsystem 用在什么场景和具备什么样的生命周期和需不需要动态加载卸载即可。
看回到 USubsystem
,可以注意到旁边的 FSubsystemCollectionBase
,这个类主要用来管理某一类型的 Subsystem ,负责 Subsystem 的创建、销毁、查询(依靠查询功能, Subsystem 可以相互之间通信)。每个类型的 Subsystem 模块都会产生一个相对应的SubsystemCollection
,例如 GameInstanceSubsystemCollection
。SubsystemCollection
底层实际会包含一个 TMap
变量,用来保存每个特定类型USubsystem
子类的实例(如 UGameInstanceSubsystem
子类的实例)。因为 TMap
会保证每个 Key
对应的 Value
唯一,而 Key
就是子类,所以在这个 Outer 对象对应的生命周期中只能创建出一个这个子类的对象。最终每个用户自定义的 Subsystem 子类生成对象都会是单例(例如用户编写了一个 UMyGameInstanceSubsystem
类,那么在GameInstance
的生命周期中只能创建一个 UMyGameInstanceSubsystem
对象)。
另外,FSubsystemCollectionBase
继承了 FGCObject
,所以FSubsystemCollection
内的对象会受到 UE4 的 GC 管理。UE4 的 GC 算法在这里不是重点,所以这里不详细说明。
另另外,上图中的 UMy*Subsystem
都是代表用户自己创建的 Subsystem ,用户编写自己的 Subsystem 的时候只需要继承特定类型的 Subsystem 即可,引擎会自动管理这些 Subsystem 的生命周期,保证和 Outer 对象的生命周期一致。
大概了解了 Subsystem 的概念、构成之后,下面开始简单说明各个类型的 Subsystem 的生命周期、作用等。因为内容比较多所以我会把重点放在比较常用的 GameInstanceSubsystem
上,毕竟实际上功能差不太多,能够弄明白 GameInstance
就基本上可以弄明白其他的 Subsystem 。如果有机会,其他的 Subsystem 后面我会详细说明它们的具体功能与实现。我们先从 USubsystem类
开始。
USubsystem 类
首先我们从上述 5 种 Subsystem 共同的基类,USubsystem
类说起。USubsystem
也继承了 UObject
,因此和其他 UObject
一样,具有反射、元数据、序列化、被 UE4 自动 GC 等功能,可以和 UObject
一样添加各类 UFUNCTION
、UPROPERTY
。这里不详细介绍 UObject
,如果感兴趣可以另外自己查询相关资料,或者有机会再另外总结,详细说明下。
首先看看基类的定义:
注意 USubsystem
被标记为了 Abstract
抽象类,所以不要尝试去实例化它。接下来,我们看看 USubsystem
定义了什么接口。
可以看出 USubsystem
这个抽象类本身是比较简单的,接口并不多,我们一个个介绍。ShouldCreateSubsystem
用来控制是否创建 Subsystem ,可以重写来自己控制什么时候创建 Subsystem ,例如我们有部分 Subsystem 是只在客户端运行的, 不希望在服务端加载,那么就可以重写这个接口,保证我们写的 Subsystem 只在客户端被实例化。
Initialize
会在 Subsystem 实例化的时候调用,我们可以重写这个接口来初始化我们的 Subsystem。注意它的参数是 FSubsystemCollectionBase& Collection
,这使得Subsystem 可以在实际上创建完成前获取到外部 Outer(这是一个 UObject
类型的指针,用来指向外部的对象,这个对象主要取决于 Subsystem 的类型,例如如果是 GameInstance
类型的那么就是指向 GameInstance
),从而获取到其他的 Subsystem 对象。
Deinitialize
则是在 Subsystem 被销毁的时候执行,我们重写这个接口可以用来善后,例如释放掉 Subsystem 正在占用的资源。
GetFunctionCallspace
主要用来查看网络状态,他的默认实现是这样的:
继续深入,可以看到 GEngine
下是这样描述的:
该函数主要用于判断当前是不是在远端调用(即运行这段代码的时候是在服务端还是在客户端),Subsystem 重写之后可以用来做网络相关的功能。
在私有变量中我们可以看到 FSubsystemCollectionBase
被声明为了友元类,这使得FSubsystemCollectionBase
中的函数可以随意访问 USubsystem
中定义的函数与成员变量(另,FSubsystemCollectionBase
继承了 FGCObject
,不然 F
开头的纯 C++ 类无法访问/管理 U
开头的 UE4 的类,如果感兴趣的话可以看一下相关的资料,这里不赘述)。
FSubsystemCollectionBase
这部分涉及到的代码太多,所以这里只是简单叙述下,不会说得太细(毕竟不是代码笔记)。后面有机会再详细解析。我们主要关注的地方是这个类怎么初始化我们的 Subsystem ,怎么销毁的。
初始化
我们前面提到了 5 类 Outer 对象,这些对象实际上自己有一个变量FSubsystemCollection
,专门用来保存 Subsystem ,例如 GameInstance
中:
又或者 World
中:
(说句题外话,看一下旁边的行数就知道完全搞懂 UE4 是不现实的…… World
这里的截图,3687 行还只是头文件的代码量)
而这些 Outer 对象在初始化的时候会把自己传入到 SubsystemCollection
的Initialize
中:
而 SubsystemCollection
继承了 SubsystemCollectionBase
,如下图:
因此最终执行的其实是 SubsystemCollectionBase
的 Initialize
:
void FSubsystemCollectionBase::Initialize(UObject* NewOuter)
{
// 省略部分代码
if (BaseType->IsChildOf(UDynamicSubsystem::StaticClass())) // 如果是 UDynamicSubsystem 的子类
{
// 省略初始化 Dynamic 类型 Subsystem 部分的代码
}
else
{ // 普通 Subsystem 对象的创建
TArray<UClass*> SubsystemClasses;
GetDerivedClasses(BaseType, SubsystemClasses, true); // 反射获得所有子类
for (UClass* SubsystemClass : SubsystemClasses)
{
AddAndInitializeSubsystem(SubsystemClass); // 添加初始化 Subsystem 对象创建
}
}
}
其中
if (BaseType->IsChildOf(UDynamicSubsystem::StaticClass()))
将这部分代码分成了两部分,在这里面条件成立的情况是支持 UDynamicSubsystem
的 Subsystem 类型的初始化代码(Editor 和 Engine 类型的 Subsystem ),其他的是较为简单的 GameInstance
、World
、LocalPlayer
类型的 Subsystem 。
非 Dynamic 类型的 Subsystem 的初始化
我们首先看比较简单的不是 Dynamic
的 Subsystem 部分,这里执行的操作实际上只有 2 步:
- 通过反射获取
BaseType
(5 种基本 Subsystem 类型)的子类; - 全部每个单独进行
AddAndInitializeSubsystem
bool FSubsystemCollectionBase::AddAndInitializeSubsystem(UClass *SubsystemClass)
{
// ...省略一些判断语句
const USubsystem *CDO = SubsystemClass->GetDefaultObject<USubsystem>();
if (CDO->ShouldCreateSubsystem(Outer)) // 从 CDO 调用 ShouldCreateSubsystem 来判断是否要创建
{
USubsystem *&Subsystem = SubsystemMap.Add(SubsystemClass); // 创建且添加到 TMap 里
Subsystem = NewObject<USubsystem>(Outer, SubsystemClass); // 创建对象
Subsystem->InternalOwningSubsystem = this; // 保存父指针
Subsystem->Initialize(*this); // 调用 Initialize
return true;
}
}
简单的说就是根据你重写的 ShouldCreateSubsystem
,以及依存的 Outer 对象,来创建 Subsystem 对象(并将 Subsystem 的持有者设定为输入的 Outer 对象),并且添加到 SubsystemMap
里面,最后调用用户重写的 Initialize
进行 Subsystem 的初始化。而且因为生成的实例保存的地方是 TMap
类型的 SubsystemMap
,所以最后可以保证每个 Subsystem 子类只生成一个实例,相当于实现了单例模式。下图是 SubsystemMap
的定义:
Dynamic
类型的 Subsystem 较为复杂点,这里只是简单介绍下大概的初始化过程。
Dynamic 类型的 Subsystem 的初始化
首先看下 DynamicSubsystem
的声明:
构造函数的实现:
可以看到,实际上没有添加功能,只是相当于用来标记一个类别而已。实际动态加载卸载的功能还是通过 FSubsystemCollectionBase
实现。
让我们回过头来看 FSubsystemCollectionBase::Initiate
:
void FSubsystemCollectionBase::Initialize(UObject *NewOuter)
{
// 省略部分检查代码
if (SubsystemCollections.Num() == 0) // SubsystemCollections 实际上是静态变量,这里通过内容数量判断是不是第一次创建
{
// 初始化 FSubsystemModuleWatcher ,监听模块的加载与卸载用
FSubsystemModuleWatcher::InitializeModuleWatcher();
}
// 省略
if (BaseType->IsChildOf(UDynamicSubsystem::StaticClass())) // 如果是 UDynamicSubsystem 的子类
{
// 注意这里的 DynamicSystemModuleMap,实际上一部分官方自己写的 Subsystem 就在这里面
for (const TPair<FName, TArray<TSubclassOf<UDynamicSubsystem>>> &SubsystemClasses : DynamicSystemModuleMap)
{
for (const TSubclassOf<UDynamicSubsystem> &SubsystemClass : SubsystemClasses.Value)
{
if (SubsystemClass->IsChildOf(BaseType))
{
AddAndInitializeSubsystem(SubsystemClass);
}
}
}
}
else
{ //普通 Subsystem 对象的创建,省略
}
}
这段代码比较简单,所以只是简单说一下做了什么。首先我们看到一开始就判断 SubsystemCollections
内容数量是不是为 0 ,这个变量在头文件 SubsystemCollection.h 的定义如下:
可以看到,是一个静态变量,所以实际上是在判断是不是第一次创建(因为引擎里有部分组件创建并添加进了这个变量之后就不会再移除,直到引擎关闭,所以可以这么干)。
可以看到原代码中,第一次创建的时候就会调用 FSubsystemModuleWatcher::InitializeModuleWatcher()
来登记每个模块用到的所有 DynamicSystem
子类。随后会把 DynamicSystemModuleMap
中记录的DynamicSubsystem
子类模版(原代码是 TArray<TSubclassOf<UDynamicSubsystem>>
)传入到函数AddAndInitializeSubsystem
中正式开始初始化。
因为 AddAndInitializeSubsystem
在上面非动态的 Subsystem 讲解中已经解释过了,就是简单地遍历并且初始化实例。所以这里着重看第一步,即 FSubsystemModuleWatcher::InitializeModuleWatcher()
的具体实现:
void FSubsystemModuleWatcher::InitializeModuleWatcher()
{
check(!ModulesChangedHandle.IsValid());
// 这里会获取所有 UDynamicSubsystem 的子类
TArray<UClass *> SubsystemClasses;
GetDerivedClasses(UDynamicSubsystem::StaticClass(), SubsystemClasses, true);
for (UClass *SubsystemClass : SubsystemClasses)
{
// 排除抽象类
if (!SubsystemClass->HasAllClassFlags(CLASS_Abstract))
{
// 获取 Subsystem 对应的包
UPackage *const ClassPackage = SubsystemClass->GetOuterUPackage();
if (ClassPackage)
{
const FName ModuleName = FPackageName::GetShortFName(ClassPackage->GetFName());
if (FModuleManager::Get().IsModuleLoaded(ModuleName))
{
// 初始化 DynamicSubsystem 并添加到静态变量 DynamicSystemModuleMap ,注意 ModuleSubsystemClasses 实际上是一个引用
TArray<TSubclassOf<UDynamicSubsystem>> &ModuleSubsystemClasses = FSubsystemCollectionBase::DynamicSystemModuleMap.FindOrAdd(ModuleName);
ModuleSubsystemClasses.Add(SubsystemClass);
}
}
}
}
// 添加监听事件,这里把函数 OnModulesChanged 与事件相关联了,这个事件是在模块加载和卸载的时候会被触发的
ModulesChangedHandle = FModuleManager::Get().OnModulesChanged().AddStatic(&FSubsystemModuleWatcher::OnModulesChanged);
}
上面的 DynamicSystemModuleMap
(出现在了 FSubsystemCollectionBase::Initialize
和FSubsystemModuleWatcher::InitializeModuleWatcher
中),是一个 static
类型变量:
主要用来记录当前动态加载的 Module 和与其对应的所有 UDynamicSubsystem
类型,与 FSubsystemModuleWatcher
相关。总之,这里只是简单的创建并按照模块来添加到 DynamicSystemModuleMap
中,后面加载和卸载模块的时候就要依赖 DynamicSystemModuleMap
来创建或者销毁模块对应的一系列DynamicSubsystem
(不用担心多个模块重复用到了某个 DynamicSubsystem
子类而导致在销毁的时候删除某个其他模块仍要使用的子类对象。因为实际上会有 GC 系统来管理这些对象,只有所有模块都不会引用某个 DynamicSubsystem
对象,这个对象才会发生 GC)。
另外,注意这里是 TArray<TSubclassOf<UDynamicSubsystem>>
。这里是 TArray
的原因是我们的模块可能会依赖多个 DynamicSubsystem
子类,模块所有要用到的 DynamicSubsystem
子类模版类都会保存在 TArray
中。我们继续看下去,看看 FSubsystemModuleWatcher::OnModulesChanged
的实现:
void FSubsystemModuleWatcher::OnModulesChanged(FName ModuleThatChanged, EModuleChangeReason ReasonForChange)
{
switch (ReasonForChange)
{
case EModuleChangeReason::ModuleLoaded:
// 创建模块
AddClassesForModule(ModuleThatChanged);
break;
case EModuleChangeReason::ModuleUnloaded:
// 销毁模块
RemoveClassesForModule(ModuleThatChanged);
break;
}
}
这个事件在每次加载或者卸载模块的时候都会触发,实际上就是依赖这个事件来实现对 DynamicSystem
子类的动态加载和卸载。
接下来我们看看创建模块的具体实现:
void FSubsystemModuleWatcher::AddClassesForModule(const FName& InModuleName)
{
// 找到模块对应的代码包
const UPackage* const ClassPackage = FindPackage(nullptr, *(FString("/Script/") + InModuleName.ToString()));
TArray<TSubclassOf<UDynamicSubsystem>> SubsystemClasses;
TArray<UObject*> PackageObjects;
// 得到模块定义的所有对象
GetObjectsWithOuter(ClassPackage, PackageObjects, false);
for (UObject* Object : PackageObjects)
{
// 尝试把包对象转成UClass类的对象
UClass* const CurrentClass = Cast<UClass>(Object);
// 确保不是空指针,不是抽象类,是UDynamicSubsystem的子类
if (CurrentClass && !CurrentClass->HasAllClassFlags(CLASS_Abstract) && CurrentClass->IsChildOf(UDynamicSubsystem::StaticClass()))
{
SubsystemClasses.Add(CurrentClass);
// 为这个类创建实例
FSubsystemCollectionBase::AddAllInstances(CurrentClass);
}
}
// 如果其内部有定义Subsystem类,那么就登记
if (SubsystemClasses.Num() > 0)
{
// 登记到DynamicSystemModuleMap静态变量里面
FSubsystemCollectionBase::DynamicSystemModuleMap.Add(InModuleName, MoveTemp(SubsystemClasses));
}
}
AddClassesForModule
的步骤可以总结为:
- 获取模块定义的所有包对象
- 将包对象转换为
UClass
类,判断是不是UDynamicSubsystem
的子类,并且不是抽象类(是的,其实你可以继承UDynamicSubsystem
并且声明为抽象类) - 第二步的判断通过,符合条件则开始用转换成
UClass
的UDynamicSubsystem
类创造实例 - 把
PackageObject
中的所有DynamicSubsystem
子类都创建好之后就会添加到静态变量FSubsystemCollectionBase::DynamicSystemModuleMap
中(代码包中可能不只是定义/引用了一个DynamicSubsystem
子类,所以存放的内容实际上是DynamicSubsystem
子类数组)
另外,创建实例的实现如下:
void FSubsystemCollectionBase::AddAllInstances(UClass* SubsystemClass)
{
for (FSubsystemCollectionBase* SubsystemCollection : SubsystemCollections)
{
if (SubsystemClass->IsChildOf(SubsystemCollection->BaseType))
{
// 前面解释过,用来创建对象
SubsystemCollection->AddAndInitializeSubsystem(SubsystemClass);
}
}
}
可以看到,最终创建实例的过程实际上就是和非动态的 Subsystem(GameInstance
、LocalPlayer
、World
)创建实例的过程是一样的。所以实际上是一开始启动的时候触发 FSubsystemModuleWatcher::InitializeModuleWatcher
,加载所有用到的 UDynamicSubsystem
子类,随后调用 FSubsystemCollectionBase::AddAndInitializeSubsystem
来 Initialize
所有 FSubsystemCollectionBase::DynamicSystemModuleMap
中的 UDyanmicSubsystem
子类,最后把生成的所有的 UDynamicSubsystem
子类实例添加到静态变量 FSubsystemCollectionBase::SubsystemMap
中。
如果是动态加载那么会直接触发事件,调用 FSubsystemCollectionBase::AddAllInstances
,最后还是调用FSubsystemCollectionBase::AddAndInitializeSubsystem
来生成实例。
再提一嘴,FSubsystemCollectionBase::DynamicSystemModuleMap
实际上是以模块划分,key
就是模块名,value
就是模块依赖的 UDynamicSubsystem
子类。单个模块可能需要用到多个 UDynamicSubsystem
,所以 value
是 TArray
类型的变量。后面加载或者释放某个模块的时候能够根据 DynamicSystemModuleMap
中的记录,知道该创建和销毁什么类型的实例。
对于 DynamicSubsystem
来说实际上多了个 FSubsystemModuleWatcher
来管理,因此实际上我们可以把关系图更新为:
销毁
实际上每个 Outer 对象销毁的时候会调用 SubsystemCollection.Deinitialize();
,例如 GameInstance
的:
Deinitialize
代码如下:
void FSubsystemCollectionBase::Deinitialize()
{
//...省略一些清除代码
for (auto Iter = SubsystemMap.CreateIterator(); Iter; ++Iter) // 遍历 Map
{
UClass* KeyClass = Iter.Key();
USubsystem* Subsystem = Iter.Value();
if (Subsystem->GetClass() == KeyClass)
{
Subsystem->Deinitialize(); // 反初始化
Subsystem->InternalOwningSubsystem = nullptr;
}
}
SubsystemMap.Empty();
Outer = nullptr;
}
可以看出,就是遍历然后逐个执行用户重写的 Deinitialize
。但是,此时 Subsystem 实际上还没有完全被 GC ,看到上面的 SubsystemMap.Empty()
了吗?还记得 Subsystem 实际上是 UObject
吗?还记得我们提到过 FSubsystemCollectionBase
继承了 FGCObject
,所以 F
开头的纯 C++ 类可以引用 U 开头的 UE4 类型对象,从而能够让 UE4 的 GC 系统管理引用的对象吗?在 FSubsystemCollectionBase
中有以下代码:
在 SubsystemMap.Empty()
后,因为保存的 Subsystem 不再被引用了,所以在下一帧 GC 系统介入的时候,会将原本保存在 Map
中的 Subsystem 对象判定为 PendingKill ,并且开始 GC 销毁这些 Subsystem 对象(另外提一嘴,实际上 UE4 也是这么处理创建的 Widget
的,所以不建议手动销毁,直接不引用,让 GC 系统处理就好了)。
Dynamic 类型 Subsystem 的销毁
void FSubsystemModuleWatcher::RemoveClassesForModule(const FName& InModuleName)
{
TArray<TSubclassOf<UDynamicSubsystem>>* SubsystemClasses = FSubsystemCollectionBase::DynamicSystemModuleMap.Find(InModuleName);
if (SubsystemClasses)
{
for (TSubclassOf<UDynamicSubsystem>& SubsystemClass : *SubsystemClasses)
{
// 销毁这个类的所有对象
FSubsystemCollectionBase::RemoveAllInstances(SubsystemClass);
}
// 移除登记
FSubsystemCollectionBase::DynamicSystemModuleMap.Remove(InModuleName);
}
}
DyanamicSubsystem
在卸载和被销毁的时候都会触发事件 OnModulesChanged
,最终调用上面这个函数,比较简单所以不解释了。比较疑惑的可能就是 FSubsystemCollectionBase::RemoveAllInstances
函数,我们看看它的具体实现:
void FSubsystemCollectionBase::RemoveAllInstances(UClass* SubsystemClass)
{
// 遍历属于该类型的实例
ForEachObjectOfClass(SubsystemClass, [](UObject* SubsystemObj)
{
USubsystem* Subsystem = CastChecked<USubsystem>(SubsystemObj);
if (Subsystem->InternalOwningSubsystem)
{
// 释放掉Subsystem实例
Subsystem->InternalOwningSubsystem->RemoveAndDeinitializeSubsystem(Subsystem);
}
});
}
可以看到实际上还是调用 FSubsystemCollectionBase::RemoveAndDeinitializeSubsystem
来遍历删除 Subsystem 子类的实例:
void FSubsystemCollectionBase::RemoveAndDeinitializeSubsystem(USubsystem* Subsystem)
{
check(Subsystem);
USubsystem* SubsystemFound = SubsystemMap.FindAndRemoveChecked(Subsystem->GetClass());
check(Subsystem == SubsystemFound);
Subsystem->Deinitialize();
Subsystem->InternalOwningSubsystem = nullptr;
}
可以看到,调用了用户重写的 Deinitialize
。
说实话,前面基本上已经说完需要说的了,因为这些不同类型的 Subsystem 实际上只是定义了一些接口,自带的功能并不多,所以以下部分都会只是很简单的介绍下。
Engine 类型的 Subsystem
UE4 里面这种 Subsystem 的类名为 UEngineSubsystem
,这类 Subsystem 和引擎一起启动,在游戏进程启动开始的时候创建,进程结束销毁,运行期间一直是全局唯一,适用于开发引擎工具。
Editor 类型的 Subsystem
和编辑器一起启动,如果是 Runtime 的游戏的话那么不会启动,只会存在编辑器下,且全局唯一。在编辑器启动的时候开始创建,编辑器退出的时候销毁。
GameInstance 类型的 Subsystem
比较常用的 Subsystem 。和游戏一起启动,游戏退出的时候销毁。只会在游戏 Runtime 或者 PIE(Play In Editor,在编辑器中启动的预览游戏场景)模式中存在。常常用于编写各类数据管理工具。例如我们有些时候希望能够有一个统一的界面管理系统,因为所有的 World
中都会用到 UI ,而且有时候切换 World
也需要显示一个加载界面的 UI ,因此不可能是 World
类型的 Subsystem 。这时候我们往往会将相关的逻辑写在一个自己创建的 GameInstanceSubsystem
子类下,因为 GameInstance
类型的 Subsystem 能够在整个游戏进行期间存在(与 GameInstance
生命周期一致),独立于 World
的加载与切换。
这类 Subsystem 只是多了一个获取 GameInstance
的函数。
World 类型的 Subsystem
和关卡 World
一起启动和销毁,数量可能大于 1(毕竟大多数游戏不止一个关卡)。生命周期和 GameMode
是一起的。
不过要注意的地方是,UE4 编辑器里面预览的场景其实也是一个 World
,所以实际上在预览场景里面可能也会创建World
类型的 Subsystem ,如果不想要你的 WorldSubsystem
在预览场景里面创建的话就要在ShouldCreateSubsystem
里面做好判断。
LocalPlayer 类型的 Subsystem
和本地玩家一起创建和销毁,数量可能大于 1(例如本地分屏多玩家类型的游戏,在多个玩家的时候就会创建多个LocalPlayer
的 Subsystem)。每个 LocalPlayer
会维护自己的 LocalPlayer
类型的 Subsystem,所以可能会有多个 ULocalPlayerSubsystem
子类实例,但是对于每个 LocalPlayer
来说都是单例。
Subsystem 的使用
Subsystem 的调用十分便利,因为官方已经包装好了相关的蓝图接口,所以在蓝图里面也可以调用 Subsystem 暴露出来的给蓝图调用函数(或者可以在 Subsystem 里面定义好 BlueprintImplementableEvent
,用 Subsystem 调用蓝图函数)。对应的 C++ 源码如下:
在蓝图中的使用:
而如果是在 C++ 中调用的话则是:
// UMyEngineSubsystem 获取
UMyEngineSubsystem* MySubsystem = GEngine->GetEngineSubsystem<UMyEngineSubsystem>();
// UMyEditorSubsystem 的获取
UMyEditorSubsystem* MySubsystem = GEditor->GetEditorSubsystem<UMyEditorSubsystem>();
// UMyGameInstanceSubsystem 的获取
UGameInstance* GameInstance = UGameplayStatics::GetGameInstance(...);
UMyGameInstanceSubsystem* MySubsystem = GameInstance->GetSubsystem<UMyGameInstanceSubsystem>();
// UMyWorldSubsystem 的获取
UWorld* World=MyActor->GetWorld(); // 也都可以用其他方式获取World
UMyWorldSubsystem* MySubsystem=World->GetSubsystem<UMyWorldSubsystem>();
// UMyLocalPlayerSubsystem 的获取
ULocalPlayer* LocalPlayer = Cast<ULocalPlayer>(PlayerController->Player)
UMyLocalPlayerSubsystem * MySubsystem = LocalPlayer->GetSubsystem<UMyLocalPlayerSubsystem>();
注意如果使用 EditorSubsystem
的话就要在工程名 .build.cs 里面加上 EditorSubsystem
模块的饮用,因为这算是编辑器模块:
// ... 省略部分内容
if (Target.bBuildEditor)
{
// 最重要的地方
PublicDependencyModuleNames.AddRange(new string[] { "EditorSubsystem" });
}
参考
- 官方 Subsystem 文档:建议直接看 UE4 源码,官方有部分地方不是特别详细(而且上面的信息不全),目前网上资料偏少,不如直接看源码
- 《InsideUE4》
GamePlay
架构(十一)Subsystems:必看,很详细,能说的基本都说了 - [英文直播] Programming Subsystems(真实字幕组)
- UE4.22 Subsystem 分析:建议一读,写得不会涉及太多细节,但是该讲的都基本覆盖到了
- 【UE4 C++】编程子系统 Subsystem
- UE4 实验使用
FGCObject
引用UObject
- 【UE4】
TSubclassOf
的使用