Skip to content

Instantly share code, notes, and snippets.

@timoxley
Last active February 26, 2025 08:12
Show Gist options
  • Save timoxley/c1c0218abe87892c33dc0e95684dad43 to your computer and use it in GitHub Desktop.
Save timoxley/c1c0218abe87892c33dc0e95684dad43 to your computer and use it in GitHub Desktop.
Subclass this to implement a world subsystem in Blueprint. Supports Timers! #unrealengine #ue5
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Editor.h"
#include "Engine/AssetManager.h"
#include "Engine/BlueprintGeneratedClass.h"
#include "Engine/World.h"
#include "Misc/CoreDelegates.h"
#include "Subsystems/EngineSubsystem.h"
#include "Subsystems/WorldSubsystem.h"
#include "MyWorldSubsystemBlueprintBase.generated.h"
/**
* Specifies the goal/source of a UWorld object
* Blueprint-visible version of EWorldType
*/
UENUM(BlueprintType)
enum class EMyWorldType : uint8
{
/** An untyped world, in most cases this will be the vestigial worlds of streamed in sub-levels */
None,
/** The game world */
Game,
/** A world being edited in the editor */
Editor,
/** A Play In Editor world */
PIE,
/** A preview world for an editor tool */
EditorPreview,
/** A preview world for a game */
GamePreview,
/** A minimal RPC world for a game */
GameRPC,
/** An editor world that was loaded but not currently being edited in the level editor */
Inactive
};
static inline bool IsImplementedInBlueprint(UObject const* Object, FName const FuncName)
{
if (!ensure(Object)) return false;
UFunction const* Func = Object->GetClass()->FindFunctionByName(FuncName);
if (!ensureMsgf(Func, TEXT("Class for object %s does not have function %s"), *GetNameSafe(Object), *FuncName.ToString()))
{
return false;
}
return (
ensure(Func->GetOuter())
&& Func->GetOuter()->IsA(UBlueprintGeneratedClass::StaticClass())
);
};
/**
* Subclass this to implement a world subsystem in Blueprint.
*
* You need to add an entry for MyWorldSubsystemBlueprintBase into: "Project Settings" → "Asset Manager" → "Primary Asset Types to Scan"
* Without this UE won't always load the BP subsystems.
*
* Remember to return any other UWorldSubsystem dependencies in an InitializeDependencies override,
* this ensures these subsystems are initialized before this subsystem.
*/
UCLASS(BlueprintType, Blueprintable, Abstract, DisplayName = "World Subsystem Blueprint Base [My]")
class MY_API UMyWorldSubsystemBlueprintBase : public UWorldSubsystem
{
GENERATED_BODY()
public:
UMyWorldSubsystemBlueprintBase()
{
bHasBPShouldCreateSubsystem = IsImplementedInBlueprint(this, GET_FUNCTION_NAME_CHECKED(ThisClass, K2_ShouldCreateSubsystem));
bHasBPDoesSupportWorldType = IsImplementedInBlueprint(this, GET_FUNCTION_NAME_CHECKED(ThisClass, K2_DoesSupportWorldType));
}
/**
* Returns a pointer to the UWorld this subsystem is contained within.
*/
UFUNCTION(BlueprintCallable, BlueprintPure, DisplayName = "GetWorld")
UWorld* K2_GetWorld() const
{
return GetWorld();
}
/**
* Implement this for deinitialization of instances of the system.
* Called when world is destroyed/changed.
*/
UFUNCTION(BlueprintImplementableEvent, DisplayName = "OnDeinitialize")
void K2_Deinitialize();
virtual void Deinitialize() override
{
Super::Deinitialize();
K2_Deinitialize();
}
/**
* Implement this for deinitialization of instances of the system.
* Called when world is destroyed/changed.
*/
UFUNCTION(BlueprintImplementableEvent, DisplayName = "OnInitialize")
void K2_Initialize();
/**
* Override this to initialize any dependencies of this Subsystem.
* This ensures subsystem dependencies are initialized before this Subsystem.
*/
UFUNCTION(BlueprintImplementableEvent, DisplayName = "InitializeDependencies")
TArray<TSubclassOf<USubsystem>> K2_InitializeDependencies();
virtual void Initialize(FSubsystemCollectionBase& Collection) override
{
for (auto const& SubsystemClass : K2_InitializeDependencies())
{
Collection.InitializeDependency(SubsystemClass);
}
Super::Initialize(Collection);
K2_Initialize();
}
/** Called once all UWorldSubsystems have been initialized */
UFUNCTION(BlueprintImplementableEvent, DisplayName = "PostInitialize")
void K2_PostInitialize();
virtual void PostInitialize() override
{
Super::PostInitialize();
K2_PostInitialize();
}
/**
* Override to control if the Subsystem should be created at all.
* For example, you could only have your system created on servers.
* It's important to note that if using this, it becomes important to null check whenever getting the Subsystem.
*
* Note: This function is called on the CDO prior to instances being created!
* You will need to restart the editor to see changes to this function.
*/
UFUNCTION(BlueprintNativeEvent, DisplayName = "ShouldCreateSubsystem")
bool K2_ShouldCreateSubsystem(UWorld* World) const;
virtual bool K2_ShouldCreateSubsystem_Implementation(UWorld* World) const
{
if (!Super::ShouldCreateSubsystem(World)) return false;
if (!DoesSupportWorldType(World->WorldType)) return false;
return true;
}
virtual bool ShouldCreateSubsystem(UObject* Outer) const override
{
if (GetClass()->HasAnyClassFlags(CLASS_Abstract)) return false; // Abstract subsystem, should never be created.
if (!bHasBPShouldCreateSubsystem) return Super::ShouldCreateSubsystem(Outer);
auto const World = CastChecked<UWorld>(Outer);
return K2_ShouldCreateSubsystem(World);
}
/**
* Called when world is ready to start gameplay before the game mode transitions to the correct state and call BeginPlay on all actors
*/
UFUNCTION(BlueprintImplementableEvent, DisplayName = "OnWorldBeginPlay")
void K2_OnWorldBeginPlay(UWorld* InWorld);
virtual void OnWorldBeginPlay(UWorld& InWorld) override
{
Super::OnWorldBeginPlay(InWorld);
K2_OnWorldBeginPlay(&InWorld);
}
/**
* Called after world components (e.g. line batcher and all level components) have been updated
*/
UFUNCTION(BlueprintImplementableEvent, DisplayName = "OnWorldComponentsUpdated")
void K2_OnWorldComponentsUpdated(UWorld* World);
virtual void OnWorldComponentsUpdated(UWorld& World) override
{
Super::OnWorldComponentsUpdated(World);
K2_OnWorldComponentsUpdated(&World);
}
/**
* Override to control if the Subsystem should be created for the given world type.
* @note You will need to restart the editor to see changes to this function.
*/
UFUNCTION(BlueprintNativeEvent, DisplayName = "DoesSupportWorldType")
bool K2_DoesSupportWorldType(EMyWorldType const WorldType) const;
virtual bool K2_DoesSupportWorldType_Implementation(EMyWorldType const WorldType) const
{
return Super::DoesSupportWorldType(static_cast<EWorldType::Type>(WorldType));
}
virtual bool DoesSupportWorldType(EWorldType::Type const WorldType) const override
{
if (!bHasBPDoesSupportWorldType) return Super::DoesSupportWorldType(WorldType);
return K2_DoesSupportWorldType(static_cast<EMyWorldType>(WorldType));
}
protected:
bool bHasBPShouldCreateSubsystem = false;
bool bHasBPDoesSupportWorldType = false;
};
/**
* This ensures any BP-subclasses of UMyWorldSubsystemBlueprintBase are loaded at editor/game startup and before entering PIE.
* Make sure to configure an entry for MyWorldSubsystemBlueprintBase in "Project Settings" → "Asset Manager" → "Primary Asset Types to Scan"
*/
UCLASS(NotBlueprintType)
class MY_API UMyWorldSubsystemBlueprintLoader : public UEngineSubsystem
{
GENERATED_BODY()
public:
void PreBeginPIE(bool bIsSimulating)
{
Load();
}
void OnInitialScanComplete()
{
Load();
}
void PostEngineInit()
{
UAssetManager& AssetManager = UAssetManager::Get();
AssetManager.CallOrRegister_OnCompletedInitialScan(FSimpleMulticastDelegate::FDelegate::CreateUObject(this, &ThisClass::OnInitialScanComplete));
}
virtual void Initialize(FSubsystemCollectionBase& Collection) override
{
Super::Initialize(Collection);
// This should always happen before PostEngineInit
FCoreDelegates::OnPostEngineInit.AddUObject(this, &ThisClass::PostEngineInit);
#if WITH_EDITOR
if (GIsEditor)
{
// ensure subclasses get loaded before PIE
FEditorDelegates::PreBeginPIE.AddUObject(this, &ThisClass::PreBeginPIE);
}
#endif
}
virtual void Deinitialize() override
{
if (LoadingHandle.IsValid() && LoadingHandle->IsActive())
{
LoadingHandle->CancelHandle();
}
LoadingHandle.Reset();
Super::Deinitialize();
}
void Load()
{
static FPrimaryAssetType const AssetType = UMyWorldSubsystemBlueprintBase::StaticClass()->GetFName();
auto const OldLoadingHandle = LoadingHandle;
ON_SCOPE_EXIT
{
// clear old handle at end to prevent cancel then re-add
if (OldLoadingHandle.IsValid())
{
OldLoadingHandle->CancelHandle();
}
};
LoadingHandle = UAssetManager::Get().LoadPrimaryAssetsWithType(AssetType);
if (!LoadingHandle.IsValid()) return; // e.g. all already loaded
LoadingHandle->WaitUntilComplete();
}
protected:
TSharedPtr<FStreamableHandle> LoadingHandle;
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment