UE4 中的 Subsystem

由 glados 发布

UE4 中的 Subsystem

在游戏开发过程中我们往往需要创建一系列的工具来辅助我们开发,例如 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;

  static UMyScoreManager* instance;

  UFUNCTION(BlueprintPure,DisplayName="MyScoreManager")
  static UMyScoreManager* Instance()
  {
    if (instance == nullptr)
    {
      instance=NewObject<UMyScoreManager>();
      instance->AddToRoot();
    }
    return instance;
  }

  UFUNCTION(BlueprintCallable)
  void AddScore(float delta);
};

// 还得要记得在.cpp头部给一个初始值
UMyScoreManager* UMyScoreManager::instance = nullptr

这就对新人很不友好了(又一个不让新人碰 C++只让写 Lua 的原因),UE4 的实现比较难看懂,而且容易出错。例如很多人会忘记加上instance->AddToRoot();,如果不记得加上,那么刚刚生成的对象可能会被 GC 掉,调用的时候会导致崩溃。而且用这种方式创建的单例会在 Editor 模式下继续存在,所以运行预览和停止预览之后并不会销毁,下一次预览的时候里面的数据可能还是上一次运行的数据。如果想要处理这个问题,就需要自己手动加上Initialize()Deinitialize()函数,手动调用,自己管理生命周期。

或者是另一种方法,直接把单例写进UGameInstance的子类里面,然后在UGameInstanceInitShutdown里面进行创建和销毁。但是即便是这样也需要手动为每一个单例类写一遍,很容易出错,也不容易维护。

总而言之,不管是什么样的实现方法,UE4 客户端开发都得要自己管理好自己写的单例类的生命周期,心智负担极大。所以官方推出了 Subsystem,并自己用在了 UE4 的部分组件的开发中(如 VaRest,官方用 Subsystem 制作了 REST API 插件),方便引擎开发、客户端开发人员对引擎或者游戏做扩展、插件,同时不用自己操心生命周期的问题。

Subsystem 时代

为什么使用 Subsystem

用 Subsystem 的好处:

  1. 不需要自己管理生命周期,引擎自动帮你管理,而且保证和指定的类型(目前只有 5 种)生命周期一致;
  2. 官方提供蓝图接口,能够很方便地在蓝图调用 Subsystem;
  3. UObject类一样,可以定义UFUNCTIONUPROPERTY
  4. 容易使用,只需继承需要的 Subsystem 类型就能够正常使用,维护成本低;
  5. 更模块化,而且可以迁移某个 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 种:

  1. Engine 类:UEngineSubsystem
  2. Editor 类:UEditorSubsystem
  3. GameInstance 类:UGameInstanceSubsystem
  4. World 类:UWorldSubsystem
  5. LocalPlayer 类:ULocalPlayerSubsystem

名称分别对应他们依存的 Outer 对象(称之为 Outer 是因为源代码里面指向这些对象的指针名为 Outer),以及他们对应的生命周期分别是:

  1. UEngine* GEngine(引擎启动期间存在,或者说游戏进程期间存在);
  2. UEditorEngine* GEditor(编辑器启动期间存在);
  3. UGameInstance* GameInstance(游戏运行期间存在);
  4. UWorld* World(关卡运行期间存在,一个游戏可能会有多个关卡,另外要注意的是编辑器下看到的场景其实也是一个 World);
  5. ULocalPlayer* LocalPlayer(本地玩家存在的时候存在,实际上通常和 GameInstance 生命周期差不多,但是可能有多个本地玩家,而且游戏进行过程中可以随时添加减少本地玩家,所以生命周期视情况、Outer 对象依附于哪个 LocalPlayer 而定);

他们其实之间没什么区别,默认的功能都较为相似,目前主要的区别在于不同类型的 Subsystem 生命周期不同。所有的 Subsystem 都直接或者间接继承了USubsystem类。我们用张图来大致展示一下各个系统的类的关系:

20220104104547

图中的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,例如GameInstanceSubsystemCollectionSubsystemCollection底层实际会包含一个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一样添加各类UFUNCTIONUPROPERTY。这里不详细介绍UObject,如果感兴趣可以另外自己查询相关资料,或者有机会再另外总结,详细说明下。

首先看看基类的定义:

20220104113720

注意USubsystem被标记为了Abstract,抽象类,所以不要尝试去实例化它。接下来,我们看看USubsystem定义了什么接口。

20220104113738

可以看出USubsystem这个抽象类本身是比较简单的,接口并不多,我们一个个介绍。ShouldCreateSubsystem用来控制是否创建 Subsystem,可以重写来自己控制什么时候创建 Subsystem,例如我们有部分 Subsystem 是只在客户端运行的, 不希望在服务端加载,那么就可以重写这个接口,保证我们写的 Subsystem 只在客户端被实例化。

Initialize会在 Subsystem 实例化的时候调用,我们可以重写这个接口来初始化我们的 Subsystem。注意他的参数是FSubsystemCollectionBase& Collection,这使得 Subsystem 可以在实际上创建完成前获取到外部 Outer(这是一个UObject类型的指针,用来指向外部的对象,这个对象主要取决于 Subsystem 的类型,例如如果是 GameInstance 类型的那么就是指向 GameInstance),从而获取到其他的 Subsystem 对象。

Deinitialize则是在 Subsystem 被销毁的时候执行,我们重写这个接口可以用来善后,例如释放掉 Subsystem 正在占用的资源。

GetFunctionCallspace主要用来查看网络状态,他的默认实现是这样的:

20220104113756

继续深入,可以看到 GEngine 下是这样描述的:

20220104113810

该函数主要用于判断当前是不是在远端调用(即运行这段代码的时候是在服务端还是在客户端),Subsystem 重写之后可以用来做网络相关的功能。
在私有变量中我们可以看到FSubsystemCollectionBase被声明为了友元类,这使得FSubsystemCollectionBase重的函数可以随意访问USubsystem中定义的函数与成员变量(另,FSubsystemCollectionBase继承了FGCObject,不然 F 开头的纯 C++类无法访问/管理 U 开头的 UE4 的类,如果感兴趣的话可以看一下相关的资料,这里不赘述)。

FSubsystemCollectionBase

这部分涉及到的代码太多,所以这里只是简单叙述下,不会说得太细(毕竟不是代码笔记)。后面有机会再详细解析。我们主要关注的地方是这个类怎么初始化我们的 Subsystem,怎么销毁的。

初始化

我们前面提到了 5 类 Outer 对象,这些对象实际上自己有一个变量FSubsystemCollection,专门用来保存 Subsystem,例如 GameInstance 中:

20220104113857

又或者 World 中:

20220104113911

(说句题外话,看一下旁边的行数就知道完全搞懂 UE4 是不现实的……World 这里的截图,3687 行还只是头文件的代码量)

而这些 Outer 对象在初始化的时候会把自己传入到SubsystemCollectionInitialize中:

20220104113933

SubsystemCollection继承了SubsystemCollectionBase,如下图:

20220104113951

因此最终执行的其实是SubsystemCollectionBaseInitialize

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部分,这里执行的操作实际上只有2步:

  1. 通过反射获取BaseType(5种基本Subsystem类型)的子类;2. 全部每个单独进行AddAndInitializeSubsystem
bool FSubsystemCollectionBase::AddAndInitializeSubsystem(UClass* SubsystemClass){
  //...省略一些判断语句
  const USubsystem* CDO = SubsystemClass->GetDefaultObject<USubsystem>();
  //从CDO调用ShouldCreateSubsystem来判断是否要创建
  if (CDO->ShouldCreateSubsystem(Outer))
  {
    //创建且添加到TMap里
    USubsystem*& Subsystem = SubsystemMap.Add(SubsystemClass);
    //创建对象
    Subsystem = NewObject<USubsystem>(Outer, SubsystemClass);
    //保存父指针
    Subsystem->InternalOwningSubsystem = this;
    //调用Initialize
    Subsystem->Initialize(*this);
    return true;
  }
}

简单的说就是根据你重写的ShouldCreateSubsystem,以及依存的Outer对象,来创建Subsystem对象(并将Subsystem的持有者设定为输入的Outer对象),并且添加到SubsystemMap里面,最后调用用户重写的Initialize进行Subsystem的初始化。而且因为生成的实例保存的地方是TMap类型的SubsystemMap,所以最后可以保证每个Subsystem子类只生成一个实例,相当于实现了单例模式。下图是SubsystemMap的定义:

20220104114015

Dynamic类型的Subsystem较为复杂点,这里只是简单介绍下大概的初始化过程。

Dynamic类型的Subsystem的初始化

首先看下DynamicSubsystem的声明:

20220104114029

构造函数的实现:

20220104114042

可以看到,实际上没有添加功能,只是相当于用来标记一个类别而已。实际动态加载卸载的功能还是通过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的定义如下:

20220104114058

可以看到,是一个静态变量,所以实际上是在判断是不是第一次创建(因为引擎里有部分组件创建并添加进了这个变量之后就不会再移除,直到引擎关闭,所以可以这么干)。

可以看到原代码中,第一次创建的时候就会调用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::InitializeFSubsystemModuleWatcher::InitializeModuleWatcher中),是一个static类型变量:

20220104114114

主要用来记录当前动态加载的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的步骤可以总结为:

  1. 获取模块定义的所有包对象
  2. 将包对象转换为UClass类,判断是不是UDynamicSubsystem的子类,并且不是抽象类(是的,其实你可以继承UDynamicSubsystem并且声明为抽象类)
  3. 第二步的判断通过,符合条件则开始用转换成UClass的UDynamicSubsystem类创造实例4. 把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来管理,因此实际上我们可以把关系图更新为:

20220104114132

销毁

实际上每个 Outer 对象销毁的时候会调用SubsystemCollection.Deinitialize();,例如 GameInstance 的:

20220104114207

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 中有以下代码:

20220104114225

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 和引擎一起启动,在游戏进程启动开始的时候创建,进程结束销毁,运行期间一直是全局唯一,适用于开发引擎工具。

20220104121324

Editor 类型的 Subsystem

和编辑器一起启动,如果是 Runtime 的游戏的话那么不会启动,只会存在编辑器下,且全局唯一。在编辑器启动的时候开始创建,编辑器退出的时候销毁。

20220104121335

GameInstance 类型的 Subsystem

比较常用的 Subsystem。和游戏一起启动,游戏退出的时候销毁。只会在游戏 Runtime 或者 PIE(Play In Editor,在编辑器中启动的预览游戏场景)模式中存在。常常用于编写各类数据管理工具。例如我们有些时候希望能够有一个统一的界面管理系统,因为所有的 World 中都会用到 UI,而且有时候切换 World 也需要显示一个加载界面的 UI,因此不可能是 World 类型的 Subsystem。这时候我们往往会将相关的逻辑写在一个自己创建的 GameInstanceSubsystem 子类下,因为 GameInstance 类型的 Subsystem 能够在整个游戏进行期间存在(与 GameInstance 生命周期一致),独立于 World 的加载与切换。

20220104121349

这类 Subsystem 只是多了一个获取 GameInstance 的函数。

World 类型的 Subsystem

和关卡 World 一起启动和销毁,数量可能大于 1(毕竟大多数游戏不止一个关卡)。生命周期和 GameMode 是一起的。

不过要注意的地方是,UE4 编辑器里面预览的场景其实也是一个 World,所以实际上在预览场景里面可能也会创建 World 类型的 Subsystem,如果不想要你的 WorldSubsystem 在预览场景里面创建的话就要在ShouldCreateSubsystem里面做好判断。

20220104121403

LocalPlayer 类型的 Subsystem

和本地玩家一起创建和销毁,数量可能大于 1(例如本地分屏多玩家类型的游戏,在多个玩家的时候就会创建多个 LocalPlayer 的 Subsystem)。每个 LocalPlayer 会维护自己的 LocalPlayer 类型的 Subsystem,所以可能会有多个 ULocalPlayerSubsystem 子类实例,但是对于每个 LocalPlayer 来说都是单例。

20220104121413

Subsystem 的使用

Subsystem 的调用十分便利,因为官方已经包装好了相关的蓝图接口,所以在蓝图里面也可以调用 Subsystem 暴露出来的给蓝图调用函数(或者可以在 Subsystem 里面定义好BlueprintImplementableEvent,用 Subsystem 调用蓝图函数)。对应的 C++源码如下:

20220104121425

在蓝图中的使用:

20220104121438

而如果是在 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" });
}

参考

1.官方Subsystem文档:建议直接看UE4源码,官方有部分地方不是特别详细(而且上面的信息不全),目前网上资料偏少,不如直接看源码
2.《InsideUE4》GamePlay架构(十一)Subsystems:必看,很详细,能说的基本都说了
3.[[英文直播]Programming Subsystems(真实字幕组)](https://www.bilibili.com/video/BV1t741187FZ)
4.UE4.22 Subsystem分析:建议一读,写得不会涉及太多细节,但是该讲的都基本覆盖到了
5.【UE4 C++】编程子系统 Subsystem
7.UE4实验使用 FGCObject 引用UObject
8.【UE4】TSubclassOf的使用


暂无评论

发表评论