【技术总结】UE4中的Subsystem

UE4中的Subsystem

TOC

在游戏开发过程中我们往往需要创建一系列的工具来辅助我们开发,例如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的子类里面,然后在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类。我们用张图来大致展示一下各个系统的类的关系:

图中的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,如果感兴趣可以另外自己查询相关资料,或者有机会再另外总结,详细说明下。

首先看看基类的定义:

注意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对象在初始化的时候会把自己传入到SubsystemCollectionInitialize中:

SubsystemCollection继承了SubsystemCollectionBase,如下图:

因此最终执行的其实是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>();
    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::InitializeFSubsystemModuleWatcher::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的步骤可以总结为:

  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来管理,因此实际上我们可以把关系图更新为:

销毁

实际上每个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++源码如下:

在蓝图中的使用:

Subsystems_01.png

而如果是在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(真实字幕组)
  4. UE4.22 Subsystem分析:建议一读,写得不会涉及太多细节,但是该讲的都基本覆盖到了
  5. 【UE4 C++】编程子系统 Subsystem
  6. UE4实验使用 FGCObject 引用UObject
  7. 【UE4】TSubclassOf的使用
posted @ 2021-07-19 10:36  夜溅樱  阅读(2609)  评论(0编辑  收藏  举报