I. Introduction

Unreal Engine introduces the Subsystem in UE4.22, which is an extension of the complicated gameplay framework. In short, the Subsystems are custom classes that could auto instantiate and destroy, there life-cycle are explicit, clear and controlled.

A subsystem can only be implemented by C++, the blueprints can only call functions of a subsystem implemented by C++.

2. What is subsystem and Why use subsystem

In general, the Subsystems are custom classes that could auto instantiate and destroy, and they form a framework which allows you to choose one from 5 classes to implement your class.

These five leaf classes are all abstract classes, users should use their derived classes implemented by themselves.

So why use a subsystem? Most of the programmers start use subsystem for finding a better singleton. In the past, we implement a singleton by the following way in Unreal Engine:

C++
UCLASS()
class HELLO_API UMyScoreManager : public UObject
{
    GENERATED_BODY()
public:
    UFUNCTION(BlueprintPure,DisplayName="MyScoreManager")
    static UMyScoreManager* Instance()
    {
        static UMyScoreManager* instance=nullptr;
        if (instance==nullptr)
        {
            instance=NewObject<UMyScoreManager>();
            instance->AddToRoot();
        }
        return instance;
        //return GetMutableDefault<UMyScoreManager>();
    }
public:
    UFUNCTION(BlueprintCallable)
    void AddScore(float delta);
public:
    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    float Score;
};
C++

You may find that this is not a classical Meyer’s Singleton, although it remains use the core idea of the Meyer’s singleton. However, it is not easy to be implemented correctly by a newbee. And what’s more, this implementation does not solve all problems:

  • Programmers who are not so experienced may forget the AddToRoot() and soon make their engine crash.
  • The singleton keep exists in both editor-time and run-time, which means the value of it is dirty. For example, you start your game in PIE(Player In Editor) mode, got 45 points in your game and then stop the game, next time you run the game the Score will be 45.
  • For those who are experienced, if we change instance=NewObject(); into instance = GetMutableObject<UMyScoreManaer>();, what would happen? The differences between them is that will it keeps the default value.
  • For those who are experienced in Sofeware-Engineer, it is not hard for you to figure out that a singleton doesn’t solve any problem, you can read the Game Programming Patterns for more information about this(In the references).

So Unreal Engine designs the Subsystem framework for better life-cycle management, we will discuss about it in detail in the following of the article, here, in brief, you can think that the life-cycle of the subsystem is as same as their outer. For example, the life-cycle of a GameInstanceSubsystem is as same as the GameInstance.

In conclusion, the subsystems provides auto instantiate and destroy, provides a better life-cycle control method.

Besides, it provides a more flexiable and modular SE framework. Of cause you could write all your codes in the GameInstance, You will soon create a monster with more than 50000+ lines and make your colleague headache. What’s more, when you are creating a plugin, you find the users of your plugin have to use the GameInstance implemented by you even though they have their own GameInstance, they have to copy and paste all the codes in your GameInstance, and soon their project explode.

By subsystems, you can write them as:

C++
UCLASS()
class HELLO_API UMyUISubsystem : public UGameInstanceSubsystem
{
    GENERATED_BODY()
public://UI System
    UFUNCTION(BlueprintCallable)
    void PushUI(TSubclassOf<UUserWidget> widgetClass);
};

UCLASS()
class HELLO_API UMyTaskSubsystem : public UGameInstanceSubsystem
{
    GENERATED_BODY()
public://Task System
    UFUNCTION(BlueprintCallable)
    void CompleteTask(FString taskName);
};

UCLASS()
class HELLO_API UMyScoreSubsystem : public UGameInstanceSubsystem
{
    GENERATED_BODY()
public://Score System
    UFUNCTION(BlueprintCallable)
    void AddScore(float delta);
};
C++

3.How to use a Subsystem

A subsystem could only derives from a C++ subsystem class, up to now we still cannot implement a subsystem in the blueprint.

Implementing a subsystem is very simple, just write like this:

C++
UCLASS()
class UMyGameInstanceSubsystem : public UGameInstanceSubsystem
{
	GENERATED_BODY()
};

UCLASS()
class UMyWorldSubsystem : public UWorldSubsystem
{
	GENERATED_BODY()

public:
	
};

UCLASS()
class UMyLocalPlayerSubsystem : public ULocalPlayerSubsystem
{
	GENERATED_BODY()

public:
	
};

UCLASS()
class UMyEditorSubsystem : public UEditorSubsystem
{
	GENERATED_BODY()

public:
	
};

UCLASS()
class UMyEngineSubsystem : public UEngineSubsystem
{
	GENERATED_BODY()

public:
	
};
C++

We take the following UScoreGameInstanceSubsystem as an example, it implements an important method: ShouldCreateSubsystem(UObject* Outer). We will figure how this method works when we dive into the source code of the subsystem soon.

C++
UCLASS()
class UScoreInstanceSubsystem : public UGameInstanceSubsystem
{
	GENERATED_BODY()

public:

	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Subsystem Test")
	int32 Score;

public:

	UScoreInstanceSubsystem();

	virtual bool ShouldCreateSubsystem(UObject* Outer) const override;

	UFUNCTION(BlueprintCallable)
	FORCEINLINE void AddScore(const int32& Value) {Score += Value;}
};
C++
C++
UScoreInstanceSubsystem::UScoreInstanceSubsystem()
{
}

/**
 * @brief If the subsystem has a child class, do not create this subsystem.
 * @param Outer 
 * @return Will the subsystem will be created.
 */
bool UScoreInstanceSubsystem::ShouldCreateSubsystem(UObject* Outer) const
{
	TArray<UClass*> SubClasses;

	GetDerivedClasses(StaticClass(), SubClasses, false);
	if (SubClasses.Num() > 0)
	{
		return false;
	}

	return true;
}
C++

You can now use this subsystem by both C++ and blueprint, the life-cycle of the subsystem is as same as the GameInstance it self, so the subsystem keep existing after the game begin, will not be destroy or clear when changing maps, so it is a good place for storing the score or inventories of the player character in a single player game.

Calling the methods of the subsystem by C++:

C++
void ASubsystemTestActor::AddScore(const int32 Score) const
{
	if(const UWorld* World = GetWorld())
	{
		if (const UGameInstance* GameInstance = World->GetGameInstance())
		{
			if (UScoreInstanceSubsystem* ScoreInstanceSubsystem = GameInstance->GetSubsystem<UScoreInstanceSubsystem>())
			{
				ScoreInstanceSubsystem->AddScore(Score);
			}
		}
	}
}
C++

Calling by blueprint:

A Subsystem coule be tickable:

C++
UCLASS()
class HELLO_API UMyTickSubsystem : public UGameInstanceSubsystem, public FTickableGameObject
{
    GENERATED_BODY()
public:
    virtual void Tick(float DeltaTime) override;
    virtual bool IsTickable() const override { return !IsTemplate(); }// IMPORTANT: Tickable if it is not CDO
    virtual TStatId GetStatId() const override{RETURN_QUICK_DECLARE_CYCLE_STAT(UMyScoreSubsystem, STATGROUP_Tickables);}
};
C++

We have learnt how to use it, but we are not satisfied, we have to figure out its pricinples.

4. How does the subsystem work

First of all, when we examine the source code of GameInstance.h, Engine.h etc, we will find such a container:

C++
	// GameInstance.h
	
	/** Remove a referenced object, this will allow it to GC out */
	virtual void UnregisterReferencedObject(UObject* ObjectToReference);

protected:
	/** Non-virtual dispatch for OnStart, also calls the associated global OnStartGameInstance. */
	void BroadcastOnStart();

	/** Called when the game instance is started either normally or through PIE. */
	virtual void OnStart();

	/** Find a map override argument on the command-line string (the first argument without a leading '-' or -map=..., whichever comes first). */
	static bool GetMapOverrideName(const TCHAR* CmdLine, FString& OverrideMapName);

private:

	FObjectSubsystemCollection<UGameInstanceSubsystem> SubsystemCollection;
};

// Engine.h

		return EngineSubsystemCollection.GetSubsystem<TSubsystemClass>(TSubsystemClass::StaticClass());
	}

	/**
	 * Get all Engine Subsystem of specified type, this is only necessary for interfaces that can have multiple implementations instanced at a time.
	 */
	template <typename TSubsystemClass>
	const TArray<TSubsystemClass*>& GetEngineSubsystemArray() const
	{
		return EngineSubsystemCollection.GetSubsystemArray<TSubsystemClass>(TSubsystemClass::StaticClass());
	}

private:
	FObjectSubsystemCollection<UEngineSubsystem> EngineSubsystemCollection;

public:
	/**
	 * Delegate we fire every time a new stat has been registered.
	 *
	 * @param FName The name of the new stat.
	 * @param FName The category of the new stat.
	 * @param FText The description of the new stat.
	 */
	DECLARE_EVENT_ThreeParams(UEngine, FOnNewStatRegistered, const FName&, const FName&, const FText&);
	static FOnNewStatRegistered NewStatDelegate;

// World.h

	FORCEINLINE FWorldPSCPool& GetPSCPool() { return PSCPool; }

	private:

	UPROPERTY()
	FWorldPSCPool PSCPool;

	//PSC Pooling END
	FObjectSubsystemCollection<UWorldSubsystem> SubsystemCollection;
};

/** Global UWorld pointer. Use of this pointer should be avoided whenever possible. */
extern ENGINE_API class UWorldProxy GWorld;

C++

The containter FObjectSusbsytemCollection derives from the FSubsystemCollectionBase, the later is a very simple class. In the following of the article, we will take the UGameInstanceSubsystem as an example and figure out how it initialize hte deinitialize.

Let’s examine the source code of the FSubsystemCollectionBase:

C++
class ENGINE_API FSubsystemCollectionBase
{
public:
	/** Initialize the collection of systems, systems will be created and initialized */
	void Initialize(UObject* NewOuter);

	/* Clears the collection, while deinitializing the systems */
	void Deinitialize();

	/** Returns true if collection was already initialized */
	bool IsInitialized() const { return Outer != nullptr; }

	/** 
	 * Only call from Initialize() of Systems to ensure initialization order
	 * Note: Dependencies only work within a collection
	 */
	USubsystem* InitializeDependency(TSubclassOf<USubsystem> SubsystemClass);

	/**
	 * Only call from Initialize() of Systems to ensure initialization order
	 * Note: Dependencies only work within a collection
	 */
	template <typename TSubsystemClass>
	TSubsystemClass* InitializeDependency()
	{
		return Cast<TSubsystemClass>(InitializeDependency(TSubsystemClass::StaticClass()));
	}

	/** Registers and adds instances of the specified Subsystem class to all existing SubsystemCollections of the correct type.
	 *  Should be used by specific subsystems in plug ins when plugin is activated.
	 */
	static void ActivateExternalSubsystem(UClass* SubsystemClass);

	/** Unregisters and removed instances of the specified Subsystem class from all existing SubsystemCollections of the correct type.
	 *  Should be used by specific subsystems in plug ins when plugin is deactivated.
	 */
	static void DeactivateExternalSubsystem(UClass* SubsystemClass);

	/** Collect references held by this collection */
	void AddReferencedObjects(UObject* Referencer, FReferenceCollector& Collector);
protected:
	/** protected constructor - for use by the template only(FSubsystemCollection<TBaseType>) */
	FSubsystemCollectionBase(UClass* InBaseType);

	/** protected constructor - Use the FSubsystemCollection<TBaseType> class */
	FSubsystemCollectionBase();
	
	/** destructor will be called from virtual ~FGCObject in GC cleanup **/
	virtual ~FSubsystemCollectionBase();

	/** Get a Subsystem by type */
	USubsystem* GetSubsystemInternal(UClass* SubsystemClass) const;

	/** Get a list of Subsystems by type */
	const TArray<USubsystem*>& GetSubsystemArrayInternal(UClass* SubsystemClass) const;

	/** Get the collection BaseType */
	const UClass* GetBaseType() const { return BaseType; }

private:
	USubsystem* AddAndInitializeSubsystem(UClass* SubsystemClass);

	void RemoveAndDeinitializeSubsystem(USubsystem* Subsystem);

	void UpdateSubsystemArrayInternal(UClass* SubsystemClass, TArray<USubsystem*>& SubsystemArray) const;

	TMap<UClass*, USubsystem*> SubsystemMap;

	mutable TMap<UClass*, TArray<USubsystem*>> SubsystemArrayMap;

	UClass* BaseType;

	UObject* Outer;

	bool bPopulating;

private:
	friend class FSubsystemModuleWatcher;

	/** Add Instances of the specified Subsystem class to all existing SubsystemCollections of the correct type */
	static void AddAllInstances(UClass* SubsystemClass);

	/** Remove Instances of the specified Subsystem class from all existing SubsystemCollections of the correct type */
	static void RemoveAllInstances(UClass* SubsystemClass);
};
C++

Functions that should be attention with firstly here are the time series functions, the Initialize method will be called when the outer object initializing, we take the GameInstanceSubsystem as an example:

C++
void UGameInstance::Init()
{
	TRACE_CPUPROFILER_EVENT_SCOPE(UGameInstance::Init);
	ReceiveInit();

	if (!IsRunningCommandlet())
	{
		UClass* SpawnClass = GetOnlineSessionClass();
		OnlineSession = NewObject<UOnlineSession>(this, SpawnClass);
		if (OnlineSession)
		{
			OnlineSession->RegisterOnlineDelegates();
		}

		if (!IsDedicatedServerInstance())
		{
			TSharedPtr<GenericApplication> App = FSlateApplication::Get().GetPlatformApplication();
			if (App.IsValid())
			{
				App->RegisterConsoleCommandListener(GenericApplication::FOnConsoleCommandListener::CreateUObject(this, &ThisClass::OnConsoleInput));
			}
		}

		FNetDelegates::OnReceivedNetworkEncryptionToken.BindUObject(this, &ThisClass::ReceivedNetworkEncryptionToken);
		FNetDelegates::OnReceivedNetworkEncryptionAck.BindUObject(this, &ThisClass::ReceivedNetworkEncryptionAck);
		FNetDelegates::OnReceivedNetworkEncryptionFailure.BindUObject(this, &ThisClass::ReceivedNetworkEncryptionFailure);

		IPlatformInputDeviceMapper& PlatformInputMapper = IPlatformInputDeviceMapper::Get();
		PlatformInputMapper.GetOnInputDeviceConnectionChange().AddUObject(this, &UGameInstance::HandleInputDeviceConnectionChange);
		PlatformInputMapper.GetOnInputDevicePairingChange().AddUObject(this, &UGameInstance::HandleInputDevicePairingChange);
	}

	SubsystemCollection.Initialize(this);
}
C++

The data member of the FSubsystemCollectionBase are:

C++
private:
	USubsystem* AddAndInitializeSubsystem(UClass* SubsystemClass);

	void RemoveAndDeinitializeSubsystem(USubsystem* Subsystem);

	void UpdateSubsystemArrayInternal(UClass* SubsystemClass, TArray<USubsystem*>& SubsystemArray) const;

	TMap<UClass*, USubsystem*> SubsystemMap;

	mutable TMap<UClass*, TArray<USubsystem*>> SubsystemArrayMap;

	UClass* BaseType;

	UObject* Outer;

	bool bPopulating;
C++

We will decribe how these memebers work in the methods of it, here I explain what are these.

The SubsystemMap is a dictionary using the class type as the key, used to find the instance of the class by the class tpye rapidly. It stores the exact class, which means that you could find the only correspounding instance by the class UNLESS it is an abstract class.

The subsystem array map is used to find all the subclasses which correspounding to the class as the key, and all the subclasses of the key class. For example, we have UBaseGameInstanceSubsystem, and UDerived1Subsystem and UDerived2Subsystem inherit form UBaseGameInstanceSubsystem, a UDerived3Subsystem inherits from UDerived1Subsystem, the SubsystemArrayMap[UBaseGameInstanceSubsystem::StaticClass()] will return an array contains all the three subsystem instances.

The BaseType emphasize which class could be held by the container, a subsystem could be held by the container if and only if it is a subclass of the BaseType:

C++
FSubsystemCollectionBase::FSubsystemCollectionBase(UClass* InBaseType)
	: BaseType(InBaseType)
	, Outer(nullptr)
	, bPopulating(false)
{
	check(BaseType);
}
C++

The bPopulating makes sure that the collection is initializing on a single thread: the game thread.

Now we dive into the time series of the subsystem, the first is the constructor, and it is empty unless it is constructed will a BaseType paramter. The initialize divide the subsystem containers into two different type: general subsystems and the dynamic subsystems. We will explain the dynamic subsystems later, here we hide the codes about the dynamic subsystem, only focus on the simplest situation:

C++
void FSubsystemCollectionBase::Initialize(UObject* NewOuter)
{
	if (Outer != nullptr)
	{
		// already initialized
		return;
	}

	Outer = NewOuter;
	check(Outer);
	if (ensure(BaseType) && ensureMsgf(SubsystemMap.Num() == 0, TEXT("Currently don't support repopulation of Subsystem Collections.")))
	{
		check(!bPopulating); //Populating collections on multiple threads?
		
		//non-thread-safe use of Global lists, must be from GameThread:
		check(IsInGameThread());

		if (GlobalSubsystemCollections.Num() == 0)
		{
			FSubsystemModuleWatcher::InitializeModuleWatcher();
		}
		
		TGuardValue<bool> PopulatingGuard(bPopulating, true);

		if (BaseType->IsChildOf(UDynamicSubsystem::StaticClass()))
		{
			for (const TPair<FName, TArray<TSubclassOf<UDynamicSubsystem>>>& SubsystemClasses : GlobalDynamicSystemModuleMap)
			{
				for (const TSubclassOf<UDynamicSubsystem>& SubsystemClass : SubsystemClasses.Value)
				{
					if (SubsystemClass->IsChildOf(BaseType))
					{
						AddAndInitializeSubsystem(SubsystemClass);
					}
				}
			}
		}
		else
		{
			TArray<UClass*> SubsystemClasses;
			GetDerivedClasses(BaseType, SubsystemClasses, true);

			for (UClass* SubsystemClass : SubsystemClasses)
			{
				AddAndInitializeSubsystem(SubsystemClass);
			}
		}

		// Update Internal Arrays without emptying it so that existing refs remain valid
		for (auto& Pair : SubsystemArrayMap)
		{
			Pair.Value.Empty();
			UpdateSubsystemArrayInternal(Pair.Key, Pair.Value);
		}

		// Statically track collections
		GlobalSubsystemCollections.Add(this);
	}
}
C++

The GlobalSubsystemCollections collects all the subsystems that have been registered. from the line 11~23 we can figure out that hte subsystem collection could only be initialized in the game thread and if it is the first subsystem to initialize, it will initialize a FSubsystemModuleWatcher at the same time.

After that, if the susbystem is not a dynamic subsystem, it will get all the subclasses of it, for each of them call the AddAndInitializeSubsystem :

C++
USubsystem* FSubsystemCollectionBase::AddAndInitializeSubsystem(UClass* SubsystemClass)
{
	TGuardValue<bool> PopulatingGuard(bPopulating, true);

	if (!SubsystemMap.Contains(SubsystemClass))
	{
		// Only add instances for non abstract Subsystems
		if (SubsystemClass && !SubsystemClass->HasAllClassFlags(CLASS_Abstract))
		{
			// Catch any attempt to add a subsystem of the wrong type
			checkf(SubsystemClass->IsChildOf(BaseType), TEXT("ClassType (%s) must be a subclass of BaseType(%s)."), *SubsystemClass->GetName(), *BaseType->GetName());

			// Do not create instances of classes aren't authoritative
			if (SubsystemClass->GetAuthoritativeClass() != SubsystemClass)
			{	
				return nullptr;
			}

			UE_SCOPED_ENGINE_ACTIVITY(TEXT("Initializing Subsystem %s"), *SubsystemClass->GetName());

			const USubsystem* CDO = SubsystemClass->GetDefaultObject<USubsystem>();
			if (CDO->ShouldCreateSubsystem(Outer))
			{
				USubsystem* Subsystem = NewObject<USubsystem>(Outer, SubsystemClass);
				SubsystemMap.Add(SubsystemClass,Subsystem);
				Subsystem->InternalOwningSubsystem = this;
				Subsystem->Initialize(*this);
				return Subsystem;
			}

			UE_LOG(LogSubsystemCollection, VeryVerbose, TEXT("Subsystem does not exist, but CDO choose to not create (%s)"), *SubsystemClass->GetName());
		}
		return nullptr;
	}

	UE_LOG(LogSubsystemCollection, VeryVerbose, TEXT("Subsystem already exists (%s)"), *SubsystemClass->GetName());
	return SubsystemMap.FindRef(SubsystemClass);
}
C++

It checks whether there already exists a subsystem instance of the given class or not, if it already exists, return it from the SubsystemMap. If there is not, and the given class is not an abstract class, it will first check the method ShouldCreateSusbsytem, it will not create the instance as well if the method returns a false. And update the SubsystemMap and call the Initialize method of the subsystem.

Then follows the method UpdateSubsystemArrayInternal, it updates the SubsystemArrayMap, which stores all the instances of all the subclasses of a give class, also contains the instance of the class itself. We emphasize that it does not clear the whole map, it only clear the array in the map to keep the references valid:

C++
void FSubsystemCollectionBase::UpdateSubsystemArrayInternal(UClass* SubsystemClass, TArray<USubsystem*>& SubsystemArray) const
{
	for (auto Iter = SubsystemMap.CreateConstIterator(); Iter; ++Iter)
	{
		UClass* KeyClass = Iter.Key();
		if (KeyClass->IsChildOf(SubsystemClass))
		{
			SubsystemArray.Add(Iter.Value());
		}
	}
}
C++

We emphasize that here it doesn’t add the class to the SubsystemArrayMap. So when does it add element to the map? The answer is: when we try to get a subsystem instance by a given type but there is no instance of it. Which means: the given type does not create a instance.

The reasons that why the type does not create an instance is complicated, maybe because it is an abstract class or return false when calling the ShouldCreateSubsystem. In short, the key of the SubsystemArrayMap are the classes that has no instance created. the value of the key are the instances of its sub classes which could create instance.

The SubsystemArrayMap here causes a special influence to the system, if we get try to get a subsystem instance by its type like UTestSubsystem, and there is no its instances in the container, it will try to return the first instance of the type that is the child class of UTestSubsystem, so it is valid that get a instance of an abstract class:

C++
USubsystem* FSubsystemCollectionBase::GetSubsystemInternal(UClass* SubsystemClass) const
{
#if WITH_EDITOR && UE_BUILD_SHIPPING
	TStringBuilder<200> DebugSubsystemClassName;
	if (SubsystemClass && IsEngineExitRequested())
	{
		SubsystemClass->GetFName().AppendString(DebugSubsystemClassName);
	}
#endif

	USubsystem* SystemPtr = SubsystemMap.FindRef(SubsystemClass);

	if (SystemPtr)
	{
		return SystemPtr;
	}
	else
	{
		const TArray<USubsystem*>& SystemPtrs = GetSubsystemArrayInternal(SubsystemClass);
		if (SystemPtrs.Num() > 0)
		{
			return SystemPtrs[0];
		}
	}

	return nullptr;
}

const TArray<USubsystem*>& FSubsystemCollectionBase::GetSubsystemArrayInternal(UClass* SubsystemClass) const
{
	if (!SubsystemArrayMap.Contains(SubsystemClass))
	{
		TArray<USubsystem*>& NewList = SubsystemArrayMap.Add(SubsystemClass);

		UpdateSubsystemArrayInternal(SubsystemClass, NewList);

		return NewList;
	}

	const TArray<USubsystem*>& List = SubsystemArrayMap.FindChecked(SubsystemClass);
	return List;
}
C++

5. Understanding the Dynamic Subsystem

Before this part make sure you have understood the modules in Unreal Engine. If you are not, this would be helpful.

Why we need a dynamic subsystem? In short, when initializing the the UWorld, UGameInstance and ULocalPlayer, all of the modules we need have been imported. But the initialization of the GEngine and GEditor are too early that the modules are still not loaded, so the engine knows nothing about the EngineSubsystem and EditorSubsystem in the modules which belong to the plugins and etc.

To avoid the problem, Unreal Engine uses the FSubsystemModuleWatcher:

C++
/** FSubsystemModuleWatcher class to hide the implementation of keeping the DynamicSystemModuleMap up to date*/
class FSubsystemModuleWatcher
{
public:
	static void OnModulesChanged(FName ModuleThatChanged, EModuleChangeReason ReasonForChange);

	/** Init / Deinit the Module watcher, this tracks module startup and shutdown to ensure only the appropriate dynamic subsystems are instantiated */
	static void InitializeModuleWatcher();
	static void DeinitializeModuleWatcher();

private:
	static void AddClassesForModule(const FName& InModuleName);
	static void RemoveClassesForModule(const FName& InModuleName);

	static FDelegateHandle ModulesChangedHandle;
};
C++

It is a static class so it does not need an instance, back to the initialization of the FSubsystemCollectionBase, The module watcher will be initialized when the first subsystem collection is initialized:

C++
void FSubsystemCollectionBase::Initialize(UObject* NewOuter)
{
	if (Outer != nullptr)
	{
		// already initialized
		return;
	}

	Outer = NewOuter;
	check(Outer);
	if (ensure(BaseType) && ensureMsgf(SubsystemMap.Num() == 0, TEXT("Currently don't support repopulation of Subsystem Collections.")))
	{
		check(!bPopulating); //Populating collections on multiple threads?
		
		//non-thread-safe use of Global lists, must be from GameThread:
		check(IsInGameThread());

		if (GlobalSubsystemCollections.Num() == 0)
		{
			FSubsystemModuleWatcher::InitializeModuleWatcher();
		}
		
		TGuardValue<bool> PopulatingGuard(bPopulating, true);

		if (BaseType->IsChildOf(UDynamicSubsystem::StaticClass()))
		{
			for (const TPair<FName, TArray<TSubclassOf<UDynamicSubsystem>>>& SubsystemClasses : GlobalDynamicSystemModuleMap)
			{
				for (const TSubclassOf<UDynamicSubsystem>& SubsystemClass : SubsystemClasses.Value)
				{
					if (SubsystemClass->IsChildOf(BaseType))
					{
						AddAndInitializeSubsystem(SubsystemClass);
					}
				}
			}
		}
		else
		{
			TArray<UClass*> SubsystemClasses;
			GetDerivedClasses(BaseType, SubsystemClasses, true);

			for (UClass* SubsystemClass : SubsystemClasses)
			{
				AddAndInitializeSubsystem(SubsystemClass);
			}
		}

		// Update Internal Arrays without emptying it so that existing refs remain valid
		for (auto& Pair : SubsystemArrayMap)
		{
			Pair.Value.Empty();
			UpdateSubsystemArrayInternal(Pair.Key, Pair.Value);
		}

		// Statically track collections
		GlobalSubsystemCollections.Add(this);
	}
}
C++

If it is a dynamic subsystem, it will be loaded from the GlobalDynamicSystemModuleMap, the elements in the later one is added when the module it belongs to loaded. So when the UEngine inits, it is still empty.

C++
void FSubsystemModuleWatcher::InitializeModuleWatcher()
{
	//non-thread-safe use of Global lists, must be from GameThread:
	check(IsInGameThread());

	check(!ModulesChangedHandle.IsValid());

	// Add Loaded Modules
	TArray<UClass*> SubsystemClasses;
	GetDerivedClasses(UDynamicSubsystem::StaticClass(), SubsystemClasses, true);

	for (UClass* SubsystemClass : SubsystemClasses)
	{
		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 = GlobalDynamicSystemModuleMap.FindOrAdd(ModuleName);
					ModuleSubsystemClasses.Add(SubsystemClass);
				}
			}
		}
	}

	ModulesChangedHandle = FModuleManager::Get().OnModulesChanged().AddStatic(&FSubsystemModuleWatcher::OnModulesChanged);
}
C++

And from the watcher, when initializing it add every dynamic subsystem into the GlobalDynamicSubsystemModuleMap and the ModuleSusbsytemClasses. However most of the dynamic susbsytems are still not loaded yet. It binds a delegate to FModuleManager::OnModulesChanged, Which works for what we need.

C++
/** FSubsystemModuleWatcher Implementations */
void FSubsystemModuleWatcher::OnModulesChanged(FName ModuleThatChanged, EModuleChangeReason ReasonForChange)
{

	switch (ReasonForChange)
	{
	case EModuleChangeReason::ModuleLoaded:
		AddClassesForModule(ModuleThatChanged);
		break;

	case EModuleChangeReason::ModuleUnloaded:
		RemoveClassesForModule(ModuleThatChanged);
		break;
	}
}
C++

It add classes and remove classes when modules changed, and the implementation here are similar to what we have discussed before, it finds every dynamic subsystem class in given module:

C++
void FSubsystemModuleWatcher::AddClassesForModule(const FName& InModuleName)
{
	//non-thread-safe use of Global lists, must be from GameThread:
	check(IsInGameThread());

	check(! GlobalDynamicSystemModuleMap.Contains(InModuleName));

	// Find the class package for this module
	const UPackage* const ClassPackage = FindPackage(nullptr, *(FString("/Script/") + InModuleName.ToString()));
	if (!ClassPackage)
	{
		return;
	}

	TArray<TSubclassOf<UDynamicSubsystem>> SubsystemClasses;
	TArray<UObject*> PackageObjects;
	GetObjectsWithPackage(ClassPackage, PackageObjects, false);
	for (UObject* Object : PackageObjects)
	{
		UClass* const CurrentClass = Cast<UClass>(Object);
		if (CurrentClass && !CurrentClass->HasAllClassFlags(CLASS_Abstract) && CurrentClass->IsChildOf(UDynamicSubsystem::StaticClass()))
		{
			SubsystemClasses.Add(CurrentClass);
			FSubsystemCollectionBase::AddAllInstances(CurrentClass);
		}
	}
	if (SubsystemClasses.Num() > 0)
	{
		GlobalDynamicSystemModuleMap.Add(InModuleName, MoveTemp(SubsystemClasses));
	}
}
void FSubsystemModuleWatcher::RemoveClassesForModule(const FName& InModuleName)
{
	//non-thread-safe use of Global lists, must be from GameThread:
	check(IsInGameThread());

	TArray<TSubclassOf<UDynamicSubsystem>>* SubsystemClasses = GlobalDynamicSystemModuleMap.Find(InModuleName);
	if (SubsystemClasses)
	{
		for (TSubclassOf<UDynamicSubsystem>& SubsystemClass : *SubsystemClasses)
		{
			FSubsystemCollectionBase::RemoveAllInstances(SubsystemClass);
		}
		GlobalDynamicSystemModuleMap.Remove(InModuleName);
	}
}
C++

6. the GC of the Subsystems

We know that every U-class can auto GC by Unreal Engine, the memory it uses will be release if the outer is clear. It uses a FGCObject to keep the references to avoid releasing the subsystem before the Collection is released:

C++
template<typename TBaseType>
class FSubsystemCollection : public FSubsystemCollectionBase, public FGCObject
{
public:
	/** Get a Subsystem by type */
	template <typename TSubsystemClass>
	TSubsystemClass* GetSubsystem(const TSubclassOf<TSubsystemClass>& SubsystemClass) const
	{
		static_assert(TIsDerivedFrom<TSubsystemClass, TBaseType>::IsDerived, "TSubsystemClass must be derived from TBaseType");

		// A static cast is safe here because we know SubsystemClass derives from TSubsystemClass if it is not null
		return static_cast<TSubsystemClass*>(GetSubsystemInternal(SubsystemClass));
	}

	/** Get a list of Subsystems by type */
	template <typename TSubsystemClass>
	const TArray<TSubsystemClass*>& GetSubsystemArray(const TSubclassOf<TSubsystemClass>& SubsystemClass) const
	{
		// Force a compile time check that TSubsystemClass derives from TBaseType, the internal code only enforces it's a USubsystem
		TSubclassOf<TBaseType> SubsystemBaseClass = SubsystemClass;

		const TArray<USubsystem*>& Array = GetSubsystemArrayInternal(SubsystemBaseClass);
		const TArray<TSubsystemClass*>* SpecificArray = reinterpret_cast<const TArray<TSubsystemClass*>*>(&Array);
		return *SpecificArray;
	}

	/* FGCObject Interface */
	virtual void AddReferencedObjects(FReferenceCollector& Collector) override
	{
		FSubsystemCollectionBase::AddReferencedObjects(nullptr, Collector);
	}

	virtual FString GetReferencerName() const override
	{
		return TEXT("FSubsystemCollection");
	}
	
public:

	/** Construct a FSubsystemCollection, pass in the owning object almost certainly (this). */
	FSubsystemCollection()
		: FSubsystemCollectionBase(TBaseType::StaticClass())
	{
	}
};

void FSubsystemCollectionBase::AddReferencedObjects(UObject* Referencer, FReferenceCollector& Collector)
{
	Collector.AddStableReferenceMap(SubsystemMap);
}
C++

References

Game Programming Patterns
gameprogrammingpatterns.com
《InsideUE4》GamePlay架构(十一)Subsystems – 知乎
大家好,我厚着脸皮又回来了引言非常惭愧,自从我更新完GamePlay架构十篇之后,我就断更了许久。如今说再多缘由也是借口,借着假期,在家继续重操旧业,继续写写技术文章。 UE在4.22版本的时候,开始引入Subsystem…
zhuanlan.zhihu.com

By JiahaoLi

Hypergryph - Game Programmer 2023 - Now Shandong University - Bachelor 2019-2023

Leave a Reply

Your email address will not be published. Required fields are marked *