Quick answer: Plain subsystems don’t tick. Inherit UTickableWorldSubsystem, or implement FTickableGameObject and override Tick, GetStatId, IsTickable.
A custom UGameInstanceSubsystem manages a timer-driven system. Its Tick override never gets called — subsystems aren’t ticked automatically.
Use UTickableWorldSubsystem
UCLASS()
class UMyWorldSubsystem : public UTickableWorldSubsystem
{
GENERATED_BODY()
public:
virtual void Tick(float DeltaTime) override;
virtual TStatId GetStatId() const override
{
RETURN_QUICK_DECLARE_CYCLE_STAT(UMyWorldSubsystem, STATGROUP_Tickables);
}
};
UTickableWorldSubsystem ticks automatically once the world is initialized.
FTickableGameObject for GameInstance Subsystems
class UMyGISubsystem : public UGameInstanceSubsystem, public FTickableGameObject
{
GENERATED_BODY()
public:
virtual void Tick(float DeltaTime) override;
virtual bool IsTickable() const override { return !IsTemplate(); }
virtual TStatId GetStatId() const override { ... }
};
FTickableGameObject auto-registers in the tickable list. IsTickable gates whether it actually ticks (skip CDOs).
Alternative: Timer or Delegate
If you don’t need per-frame tick, use a repeating timer:
GetWorld()->GetTimerManager().SetTimer(Handle, this, &UMySubsystem::Update, 0.1f, true);
Cheaper than per-frame for systems that only need 10Hz updates.
Verifying
Add a log in Tick. It fires every frame after world init. IsTickable correctly returns false for the CDO.
“Subsystems don’t tick out of the box. Inherit the tickable variant or implement FTickableGameObject.”
Prefer timers over per-frame tick wherever the system tolerates it — tick budget is precious, and most managers don’t need 60Hz.