[UE5] Choosing the Right Subsystem: Five Lifetimes and Scopes

Created: 2026-02-07Last updated: 2026-07-20

An illustrated guide to UE's five Subsystem types and their lifetimes. Covers the differences between GameInstance, World, LocalPlayer, Engine, and Editor, the advantages over hand-rolled singletons and stuffing everything into GameInstance, the Blueprint getter nodes, and a hands-on score manager that survives level transitions.

Save data, settings values, a score carried across stages. Some things need to be reachable from anywhere but do not fit naturally on any actor. Start adding variables to GameInstance and six months later you have a class with dozens of them.

UE provides a home built for exactly this: the Subsystem. This article covers the lifetime differences between the five Subsystem types, what you gain over hand-rolled singletons and GameInstance, how to use them from Blueprint, and how to build a score manager that survives level transitions.

Five shelves in a row, each holding a horizontal bar of a different length representing its lifetime

What You'll Learn

  • What a Subsystem really is: a per-feature home that the engine creates and destroys for you
  • The differences between the five lifetimes and how to choose
  • Why GameInstance Subsystems are the ones you use most
  • The concrete advantages over hand-rolled singletons and GameInstance variables
  • How to use them from Blueprint (pulling them out with a Get node)
  • Hands-on: managing a score that survives level transitions

Sponsored

Why You Need Them: Some Things Have Nowhere to Live

While making a game, you run into features that belong to no actor.

  • Values like total score or currency that should not vanish when you change levels
  • Settings like volume or key bindings that any screen needs to read and write
  • Systems like save and load where one instance is all you ever need

GameInstance is the first place that comes to mind, but pile everything in there and unrelated features end up sharing one class. You open a file to fix the score logic and find volume and save code sitting in it. The Blueprint-only way to hold values on GameInstance is covered in Carrying Data Across Levels with Game Instance; once that place gets cluttered, the Subsystem in this article is the next step.

Left: a GameInstance box overflowing with score, volume, save, and achievements. Right: four small boxes hanging separately beneath GameInstance

Subsystems provide that separation as an engine feature. The essence is that you can add a box per feature without changing a single line of GameInstance. The engine handles creating and destroying the boxes, so you only write what goes inside.


The Five Lifetimes

There are five Subsystem types, and the only difference is what they hang off and how long they live. You write all of them the same way, so this is the one thing to look at when choosing.

Five horizontal bars comparing lifetimes. Engine is the longest, followed by Editor, GameInstance, LocalPlayer, and World. Only World is cut off at a level transition
TypeBase class to inheritHow long it livesGood for
GameInstanceUGameInstanceSubsystemGame launch to shutdown (survives level transitions)Score, settings, save management, achievements
WorldUWorldSubsystemLevel load to unloadEnemy spawn management, level-specific progression
LocalPlayerULocalPlayerSubsystemPlayer added to removedPer-player UI settings, input assignments
EngineUEngineSubsystemEngine startup to shutdownA single low-level system for the whole engine
EditorUEditorSubsystemEditor startup to shutdownEditor extensions, authoring tools

The top three are the ones you use in gameplay, and GameInstance Subsystems are by far the most common. Surviving level transitions matches exactly what "I want to carry this over" needs.

Thinking through them in this order keeps you from hesitating.

  1. Should it survive a level change? → yes means GameInstance
  2. Is it contained within one level? → yes means World
  3. Do you want a separate one per player? → yes means LocalPlayer

It's easier to picture across a single roguelite: the currency you carry through a run is GameInstance, the enemy spawn management for the floor you're on is World, and the UI settings for just the split-screen P2 side are LocalPlayer. Even within one game, the place to store something is decided by how long it needs to last.

Note: Engine and Editor Subsystems are for building editor-side tooling rather than game content. Putting gameplay features in them can behave unexpectedly in a packaged build, so sticking to the top three at first is plenty.

Sponsored

What You Gain Over a Hand-Rolled Singleton

You can build "one home reachable from anywhere" by writing your own singleton. The reason to use a Subsystem anyway is that the engine takes the tedious parts off your hands.

ConcernHand-rolled singletonSubsystem
When it is createdYou write it. Initialization-order bugs are commonThe engine calls Initialize()
When it is destroyedYou write it. Forget to clear it and values leak into the next runThe engine calls Deinitialize()
State in PIE (editor play)Values from the previous run can stick aroundRebuilt per GameInstance
Using it from BlueprintYou write your own getter functionA dedicated Get node exists from the start
Dependency orderAdjusted by handDeclared with Collection.InitializeDependency<T>()
Left: a hand-rolled singleton holding its own create and destroy cards, with values from the previous run still present on the second PIE. Right: the engine's hands holding Initialize and Deinitialize, rebuilding the Subsystem clean every time

The one that really bites is being rebuilt in PIE. Your own static storage can keep values after you stop play in the editor and carry them into the next run, which causes hard-to-trace bugs like "only the second Play behaves strangely." Subsystems remove that worry.

The advantage over writing directly into GameInstance is in the same place: as features grow, what grows is the number of files, not the line count of GameInstance.


Creating One in C++

Creating a Subsystem requires C++. Open Tools > New C++ Class..., and in the parent class selection, switch to All Classes and type GameInstanceSubsystem in the search field (for how to create C++ classes, see the first-step article).

// ScoreSubsystem.h
#pragma once

#include "CoreMinimal.h"
#include "Subsystems/GameInstanceSubsystem.h"
#include "ScoreSubsystem.generated.h"

UCLASS(BlueprintType)
class MYPROJECT_API UScoreSubsystem : public UGameInstanceSubsystem
{
    GENERATED_BODY()

public:
    // Called automatically by the engine. Use it in place of a constructor
    virtual void Initialize(FSubsystemCollectionBase& Collection) override;
    virtual void Deinitialize() override;

    UFUNCTION(BlueprintCallable, Category = "Score")
    void AddScore(int32 Amount);

    UFUNCTION(BlueprintPure, Category = "Score")
    int32 GetTotalScore() const { return TotalScore; }

private:
    int32 TotalScore = 0;
};
// ScoreSubsystem.cpp
#include "ScoreSubsystem.h"

void UScoreSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
    Super::Initialize(Collection);
    TotalScore = 0;
}

void UScoreSubsystem::Deinitialize()
{
    Super::Deinitialize();
}

void UScoreSubsystem::AddScore(int32 Amount)
{
    TotalScore += Amount;
}

Three things to remember.

  • Initialize() is your constructor replacement. Set initial values and register with other systems here
  • Deinitialize() is for cleanup. It can be empty if you hold nothing
  • Overriding ShouldCreateSubsystem(UObject* Outer) lets you decide conditionally whether to create it. Useful for Subsystems you only need in specific situations

Retrieving it from C++ is one line.

if (UGameInstance* GI = GetGameInstance())
{
    UScoreSubsystem* Score = GI->GetSubsystem<UScoreSubsystem>();
    Score->AddScore(100);
}
Sponsored

Using It from Blueprint

C++ is only required to create one. Using it is entirely doable from Blueprint, and plenty of people assume otherwise and never find out.

A Blueprint graph with a "Get Score Subsystem" node wired into Add Score

Right-click in the event graph and search for the class name. For UScoreSubsystem, a node called Get Score Subsystem appears. Drag off the blue reference pin and every function marked BlueprintCallable is right there.

BP_Enemy (when an enemy is defeated)
Event Destroyed
  → Get Score Subsystem
  → Add Score (Amount: 100)

WBP_HUD (display side, just confirming you can get it)
Event Construct → Get Score Subsystem → Get Total Score → Set Text (show current value once)

You'll be tempted to read it every frame with Event Tick on the display side, but we avoid that here. Score only needs updating when it changes, so the hands-on uses a notification the instant score changes (Bind) instead of Event Tick.

There is also a generic Get Game Instance Subsystem node where you specify the Subsystem type on the Class pin. Equivalent nodes exist for World and LocalPlayer.

Note: If the node does not show up in the search, check that the class has UCLASS(BlueprintType) and that the build succeeded. Header changes are not applied through Live Coding, so close the editor and rebuild.


Hands-On: A Score That Survives Level Transitions

Cumulative score in a stage-based shooter, currency during a roguelike run, series points in a racing game. "I want the number to carry over when the level changes" comes up in every genre.

Write it in GameMode or a level Blueprint and it resets to 0 the moment you change levels, because GameMode is recreated per level (see the game framework article). Put it in a GameInstance Subsystem and the problem disappears.

The Finished Result

A flow showing 300 points earned in Level1, the display staying at 300 after Open Level moves to Level2, then reaching 400 when 100 more is added. Below is a comparison showing it resetting to 0 if stored in GameMode

Setup

Give UScoreSubsystem (parent class: UGameInstanceSubsystem) the following members.

MemberTypeInitial valueSpecifiers
TotalScoreint320private (not exposed to BP)
HighScoreint320private
OnScoreChangedFOnScoreChangedSignature (one int32)BlueprintAssignable
AddScore(int32 Amount)no return valueBlueprintCallable
ResetRun()no return valueBlueprintCallable
GetTotalScore()const function returning int32BlueprintPure

The key detail is that TotalScore is never exposed directly and can only be changed through AddScore. With exactly one place that can write the value, you never have to hunt for "something is resetting this to zero."

C++ Side: The Complete Code

// ScoreSubsystem.h
#pragma once

#include "CoreMinimal.h"
#include "Subsystems/GameInstanceSubsystem.h"
#include "ScoreSubsystem.generated.h"

// Delegate that announces the score changed
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnScoreChangedSignature, int32, NewTotalScore);

UCLASS(BlueprintType)
class MYPROJECT_API UScoreSubsystem : public UGameInstanceSubsystem
{
    GENERATED_BODY()

public:
    virtual void Initialize(FSubsystemCollectionBase& Collection) override;
    virtual void Deinitialize() override;

    // An event Blueprint can bind to
    UPROPERTY(BlueprintAssignable, Category = "Score")
    FOnScoreChangedSignature OnScoreChanged;

    // Add points. Ignores zero and negative values
    UFUNCTION(BlueprintCallable, Category = "Score")
    void AddScore(int32 Amount);

    // Call at the start of a run. Keeps the high score
    UFUNCTION(BlueprintCallable, Category = "Score")
    void ResetRun();

    UFUNCTION(BlueprintPure, Category = "Score")
    int32 GetTotalScore() const { return TotalScore; }

    UFUNCTION(BlueprintPure, Category = "Score")
    int32 GetHighScore() const { return HighScore; }

private:
    int32 TotalScore = 0;
    int32 HighScore = 0;
};
// ScoreSubsystem.cpp
#include "ScoreSubsystem.h"

void UScoreSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
    Super::Initialize(Collection);

    TotalScore = 0;
    HighScore = 0;

    UE_LOG(LogTemp, Log, TEXT("ScoreSubsystem initialized."));
}

void UScoreSubsystem::Deinitialize()
{
    UE_LOG(LogTemp, Log, TEXT("ScoreSubsystem deinitialized. High: %d"), HighScore);

    Super::Deinitialize();
}

void UScoreSubsystem::AddScore(int32 Amount)
{
    if (Amount <= 0)
    {
        return;
    }

    TotalScore += Amount;

    if (TotalScore > HighScore)
    {
        HighScore = TotalScore;
    }

    UE_LOG(LogTemp, Log, TEXT("Score +%d -> Total: %d"), Amount, TotalScore);

    // Notify the display side. It does not know who is listening
    OnScoreChanged.Broadcast(TotalScore);
}

void UScoreSubsystem::ResetRun()
{
    TotalScore = 0;
    OnScoreChanged.Broadcast(TotalScore);
}

Blueprint Side: The Caller and the Display

The scoring side is one wire from the enemy's defeat logic.

BP_Enemy (on defeat)
Event Destroyed
  → Get Score Subsystem → Add Score (Amount: 100)

Note: Event Destroyed fires for destruction of any kind, including a world teardown on level transition, not just a defeat. To make "score only on a kill" strict, fire a dedicated event (such as OnKilled) from the defeat logic and add score from there. Here we use a simple Event Destroyed to see it working.

The display side binds to OnScoreChanged instead of reading every frame on Tick. It updates only when the score actually changes, which fits straight into a Tick-free design.

WBP_ScoreHUD (display side)
Event Construct
  → Get Score Subsystem
  → Bind Event to On Score Changed
      Event pin ← Custom Event: HandleScoreChanged (receives New Total Score)
HandleScoreChanged → Set Text (Text: New Total Score converted to a string)

Event Destruct
  → Get Score Subsystem → Unbind Event from On Score Changed

Always write the Unbind after a Bind. The reason is covered in the Event Dispatcher article.

Verify

Place three enemies in Level1, defeat them all, then move to the next level with Open Level (Level Name: Level2).

  • Defeating three enemies in Level1 moves the HUD 100 → 200 → 300
  • After Open Level takes you to Level2, the HUD still reads 300. This is the one thing you are checking
  • Defeating one enemy in Level2 makes it 400
  • The Output Log shows ScoreSubsystem initialized. exactly once at the start of Play, and never on level changes
  • Calling ResetRun from an R key for testing returns TotalScore to 0, while GetHighScore stays at 400

If it resets to 0 after the transition, the points are most likely going into GameMode or GameState rather than the Subsystem. If ScoreSubsystem initialized. prints on every level change, you inherited from UWorldSubsystem instead of UGameInstanceSubsystem. For how to read the log, see the debugging article.

Two things to take away.

  • "Do I want it carried over" decides the base class: UGameInstanceSubsystem and UWorldSubsystem differ by one word, but whether the data survives a level transition is a completely different outcome. When unsure, ask yourself "do I want this remembered until I stop Play?"
  • Do not expose the value; change it through functions: making TotalScore private and exposing only AddScore fixes the number of places that can write it at one. The more globally reachable a home is, the more it pays to narrow the entrance
Sponsored

Bonus: Good to Know for Later

  • Creating a Subsystem requires C++: you cannot make one in Blueprint alone. If you are not ready to add C++, a Blueprint subclass of GameInstance works as a stand-in, and you can migrate to Subsystems once the feature count grows
  • Tick does not run by default: if your Subsystem needs per-frame work, inherit from UTickableWorldSubsystem. If it does not, skipping that avoids adding a pointless Tick (see Tick-free design)
  • Do not have Subsystems call each other directly: entangled dependencies bring initialization-order problems right back. If you genuinely need an order, call Collection.InitializeDependency<UOtherSubsystem>() inside Initialize to guarantee the other one comes first
  • This is not the same as saving: a Subsystem only remembers things until the game shuts down. Values you want on the next launch have to be written to disk with a save and load system
  • World Subsystems are created more often than you expect: they can be spawned for editor preview worlds too. If you want to restrict one to actual gameplay, write the condition in ShouldCreateSubsystem()

Summary

  • A Subsystem is a per-feature home that the engine creates and destroys for you, letting you add features without touching GameInstance
  • The five types differ only in lifetime. For gameplay, look at GameInstance / World / LocalPlayer
  • The most common by far is the GameInstance Subsystem, since it is not destroyed on level transitions and becomes the home for anything you carry over
  • The difference from a hand-rolled singleton is that the engine handles initialization, destruction, and rebuilding in PIE
  • Creating one takes C++, but using it is entirely doable in Blueprint (the Get ... Subsystem node)
  • The core of the hands-on: use GameInstance if you want it carried over, and keep values private behind functions

Take a look at the variables currently sitting in your GameInstance or GameMode. Are the ones you want to survive level transitions mixed in with the ones you do not? That boundary is exactly how to split them into Subsystems.