Last active
February 26, 2025 08:12
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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