Reference UE 5.7Versioned

ShouldCreateSubsystem and the subsystem class hierarchy

The subsystem collection asks each non-abstract class's CDO whether to create an instance. A subclass does not automatically replace its parent; both can exist side by side. This explains how to move the implementation into Blueprint, how to keep a C++ fallback when the child is missing, and why GetSubsystem<T>() can still find the derived instance.

Table of contents

ShouldCreateSubsystem reads like an on/off switch: return false, and the subsystem is not created. That is how it behaves in its most common use, but that is only one use of a broader mechanism. The method decides whether an instance of each candidate class is created. Across a hierarchy, those individual decisions determine which implementations end up in the collection.

The difference only shows up once the hierarchy has more than one level, which happens as soon as someone creates a Blueprint child of a subsystem written in C++.

I covered the lifecycle contract and where each collection starts in a separate post: Subsystem and manager actor lifecycles. Here, I’m focusing on one specific part: the loop that picks the classes.

A subclass does not replace its parent

The collection gathers derived classes in a single call and runs every one of them through the same procedure (Engine/Private/Subsystems/SubsystemCollection.cpp:209):

TArray<UClass*> SubsystemClasses;
GetDerivedClasses(BaseType, SubsystemClasses, true);

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

There is no selection of the most-derived class here, no collapsing of the hierarchy, no rule that the child wins over the parent. The question is asked separately of each entry’s own CDO (SubsystemCollection.cpp:331):

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

The consequence is straightforward and easy to miss: a Blueprint child of a C++ class joins the parent as a second, independent instance. The base implementation returns true (Subsystem.h:61), so if nobody has overridden the method, two instances live in the collection. One of the C++ class, one of the Blueprint class. Both have run Initialize, both carry their own state, and neither knows about the other.

Warning
Creating a Blueprint child of a subsystem takes two clicks and no C++ at all. From that point on, every counter, cache, and handler in that subsystem exists in duplicate. The engine reports nothing, because as far as the collection is concerned these are two different classes and two valid entries in SubsystemMap.

Two filters run before that question is asked. The first rejects abstract classes (:318), the second rejects non-authoritative ones (:324):

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

The second one says something about intent. GetAuthoritativeClass filters out the intermediate classes produced when Blueprints are recompiled, which means Epic hardened this path for Blueprint classes. A Blueprint subsystem is a scenario the engine anticipates.

The switch: false in C++, true in Blueprint

The question is asked separately of every CDO, and Blueprint defaults live on the CDO of their class. The same flag can therefore hold a different value at each level of the hierarchy, which turns it into a switch between implementation layers:

UCLASS(Blueprintable)
class MYGAME_API UMySubsystem : public UGameInstanceSubsystem
{
    GENERATED_BODY()

protected:
    /** Read from the CDO only - set to true in the Blueprint child that implements this. */
    UPROPERTY(EditDefaultsOnly, Category = "Config")
    bool bShouldCreateSubsystem = false;

    virtual bool ShouldCreateSubsystem(UObject* Outer) const override
    {
        return Super::ShouldCreateSubsystem(Outer) && bShouldCreateSubsystem;
    }
};

With false on the C++ side and true in the Blueprint class defaults, exactly one instance ends up in the collection. The C++ class becomes a skeleton: it defines the interface, the BlueprintImplementableEvent declarations, and everything Blueprint cannot declare on its own, while the implementation lives one level below.

This works because ShouldCreateSubsystem runs on the CDO before an instance exists, which means it runs on the object that holds the default values stored in the Blueprint asset. That is also why the flag is marked EditDefaultsOnly. The value is read from the defaults and nowhere else, so per-instance editing changes nothing.

Note
The result of Super::ShouldCreateSubsystem(Outer) has to feed into the condition. The base implementation returns true (Subsystem.h:61), so calling it and discarding the result looks harmless. In classes derived from UWorldSubsystem the override checks DoesSupportWorldType, and skipping it allows the subsystem to be created in editor worlds.

GetSubsystem<T>() finds the child anyway

Instances live in a TMap<UClass*, USubsystem*> keyed by the concrete class. The natural conclusion is that querying for the base class will not find an instance of a derived one, and that conclusion is wrong. GetSubsystemInternal has a fallback (SubsystemCollection.cpp:66):

USubsystem* SystemPtr = SubsystemMap.FindRef(SubsystemClass);

if (SystemPtr)
{
    return SystemPtr;
}
else
{
    const FSubsystemArray& SystemPtrs = FindAndPopulateSubsystemArrayInternal(SubsystemClass);
    if (SystemPtrs.Subsystems.Num() > 0)
    {
        return SystemPtrs.Subsystems[0];
    }
}

A miss in the map triggers an IsChildOf sweep and returns the first hit. So GetSubsystem<UMySubsystem>() with the base class as the parameter returns the Blueprint child instance, even though nothing is stored under the base class key.

Without that fallback the switch would make no sense. Every piece of C++ that queries the subsystem by its own base class would stop seeing it the moment the implementation moved into Blueprint.

Insight
The fallback returns Subsystems[0], the first hit in the array. With two instances alive (a parent that never overrode the method, plus a child), the order in that array decides the result. That is one more reason for the hierarchy to have exactly one winner.

Listing every instance at once takes a separate accessor. In 5.7 it is called GetSubsystemArrayCopy and returns by value, both on the collection itself (Public/Subsystems/SubsystemCollection.h:139) and on the UGameInstance wrapper (Classes/Engine/GameInstance.h:463).

The flagless variant: yielding to subclasses

The flag has one unpleasant property. When the Blueprint child is gone, whether it was deleted, moved, or simply never loaded in time, false on the C++ side means nothing is created. The subsystem disappears entirely, including the part that was written in C++ and had nothing to do with Blueprint.

The condition can be phrased differently: let the class step aside, provided there is someone to step aside for.

bool UMySubsystem::ShouldCreateSubsystem(UObject* Outer) const
{
    if (!Super::ShouldCreateSubsystem(Outer))
    {
        return false;
    }

    /// @Note: The collection instantiates every non-abstract subclass, so a Blueprint child
    ///        would live alongside this one. Yield so that only the most-derived class is created.
    TArray<UClass*> DerivedClasses;
    GetDerivedClasses(GetClass(), DerivedClasses, /*bRecursive*/ true);
    return DerivedClasses.Num() == 0;
}

In the normal case the outcome is the same, since a child exists and the parent steps aside. The failure looks different though: a missing child leaves a working C++ instance instead of an empty slot. There is also no flag to forget to set in a new Blueprint’s defaults.

I use this variant in a production subsystem, and I have tested it in PIE. With the Blueprint child present, exactly one instance is alive; with the child removed, the C++ instance is. I have not tested it in a packaged build, so that behavior remains an inference from the mechanism rather than an observed result.

There are two limits, and both follow directly from this implementation.

Two children mean two instances. Each of them sees zero descendants and each concludes that it is the most-derived one. This case cannot be resolved inside the method, which leaves a convention: exactly one Blueprint child per class.

A single permanently loaded C++ subclass disables the base class for good. A test class in an Editor module is enough. It is compiled, so it sits in memory from startup, so the parent yields to it in every editor session. In practice this means a test module must not derive from such a subsystem, and the test double has to be built on a plain UObject acting as a listener. This trap is worse than two Blueprint children, because nothing about it is visible in the assets. It lives in test code that nobody associates with runtime.

Common mistakes

MistakeEffect
Super::ShouldCreateSubsystem(Outer); on its own line, result discardedThe parent condition does nothing; in UWorldSubsystem the subsystem enters editor worlds
EditAnywhere instead of EditDefaultsOnly on the flagImplies per-instance editing, while the value is read from the CDO and nowhere else
A Blueprint child with no overridden condition on the C++ sideTwo live instances, split state, and GetSubsystem<T>() returning the first hit in the array
The flag set to false and a child that never loads in timeZero instances — a separate case, covered here

The last of those cannot be reproduced in the editor. It only surfaces in a packaged build.

Verification

The number and classes of live instances, in one console command:

obj list class=MySubsystem

A query for the base class covers derived classes, so it shows the parent and the children at once, broken down by class. Two rows instead of one reveal the duplication described in the table above; a row for a _C class means the switch worked and the Blueprint implementation is the one running.

Each CDO decision is in the log once the category’s log level is raised:

-LogCmds="LogSubsystemCollection VeryVerbose"
LogSubsystemCollection: VeryVerbose: Subsystem does not exist, but CDO choose to not create (MySubsystem)

That line means the class was on the list and refused. Its absence, together with a missing instance, means something entirely different: the class was not on the list, so nobody asked it. Telling those two states apart is the only way to distinguish a working switch from a class that never loaded. At the default log level both of them look like silence, which is why the category’s log level has to be raised first.

See also

Related pages