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.
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
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.

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.

| Type | Base class to inherit | How long it lives | Good for |
|---|---|---|---|
| GameInstance | UGameInstanceSubsystem | Game launch to shutdown (survives level transitions) | Score, settings, save management, achievements |
| World | UWorldSubsystem | Level load to unload | Enemy spawn management, level-specific progression |
| LocalPlayer | ULocalPlayerSubsystem | Player added to removed | Per-player UI settings, input assignments |
| Engine | UEngineSubsystem | Engine startup to shutdown | A single low-level system for the whole engine |
| Editor | UEditorSubsystem | Editor startup to shutdown | Editor 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.
- Should it survive a level change? → yes means GameInstance
- Is it contained within one level? → yes means World
- 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.
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.
| Concern | Hand-rolled singleton | Subsystem |
|---|---|---|
| When it is created | You write it. Initialization-order bugs are common | The engine calls Initialize() |
| When it is destroyed | You write it. Forget to clear it and values leak into the next run | The engine calls Deinitialize() |
| State in PIE (editor play) | Values from the previous run can stick around | Rebuilt per GameInstance |
| Using it from Blueprint | You write your own getter function | A dedicated Get node exists from the start |
| Dependency order | Adjusted by hand | Declared with Collection.InitializeDependency<T>() |

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 hereDeinitialize()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);
}
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.

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

Setup
Give UScoreSubsystem (parent class: UGameInstanceSubsystem) the following members.
| Member | Type | Initial value | Specifiers |
|---|---|---|---|
TotalScore | int32 | 0 | private (not exposed to BP) |
HighScore | int32 | 0 | private |
OnScoreChanged | FOnScoreChangedSignature (one int32) | — | BlueprintAssignable |
AddScore(int32 Amount) | no return value | — | BlueprintCallable |
ResetRun() | no return value | — | BlueprintCallable |
GetTotalScore() | const function returning int32 | — | BlueprintPure |
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 Destroyedfires 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 asOnKilled) from the defeat logic and add score from there. Here we use a simpleEvent Destroyedto 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
ResetRunfrom anRkey for testing returnsTotalScoreto 0, whileGetHighScorestays 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:
UGameInstanceSubsystemandUWorldSubsystemdiffer 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
TotalScoreprivate and exposing onlyAddScorefixes 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
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>()insideInitializeto 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.