Added CommonLoadingScreen

This commit is contained in:
LongLy 2025-01-06 14:27:00 -07:00
parent 799cfa4303
commit 6814732c4b
17 changed files with 1267 additions and 0 deletions

View File

@ -0,0 +1,29 @@
{
"FileVersion": 3,
"Version": 1,
"VersionName": "1.0",
"FriendlyName": "CommonLoadingScreen",
"Description": "Loading screen manager handling creation and display of a project-specified loading screen widget",
"Category": "Gameplay",
"CreatedBy": "Epic Games, Inc.",
"CreatedByURL": "https://www.epicgames.com",
"DocsURL": "",
"MarketplaceURL": "",
"SupportURL": "",
"CanContainContent": false,
"IsBetaVersion": false,
"IsExperimentalVersion": false,
"Installed": false,
"Modules": [
{
"Name": "CommonLoadingScreen",
"Type": "Runtime",
"LoadingPhase": "Default"
},
{
"Name": "CommonStartupLoadingScreen",
"Type": "ClientOnly",
"LoadingPhase": "PreLoadingScreen"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -0,0 +1,57 @@
// Copyright Epic Games, Inc. All Rights Reserved.
using UnrealBuildTool;
public class CommonLoadingScreen : ModuleRules
{
public CommonLoadingScreen(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs;
PublicIncludePaths.AddRange(
new string[] {
// ... add public include paths required here ...
}
);
PrivateIncludePaths.AddRange(
new string[] {
// ... add other private include paths required here ...
}
);
PublicDependencyModuleNames.AddRange(
new string[]
{
"Core",
// ... add other public dependencies that you statically link with here ...
}
);
PrivateDependencyModuleNames.AddRange(
new string[]
{
"CoreUObject",
"Engine",
"Slate",
"SlateCore",
"InputCore",
"PreLoadScreen",
"RenderCore",
"DeveloperSettings",
"UMG"
}
);
DynamicallyLoadedModuleNames.AddRange(
new string[]
{
// ... add any modules that your module loads dynamically here ...
}
);
}
}

View File

@ -0,0 +1,5 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "Modules/ModuleManager.h"
IMPLEMENT_MODULE(FDefaultModuleImpl, CommonLoadingScreen)

View File

@ -0,0 +1,12 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "CommonLoadingScreenSettings.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(CommonLoadingScreenSettings)
UCommonLoadingScreenSettings::UCommonLoadingScreenSettings()
{
CategoryName = TEXT("Game");
}

View File

@ -0,0 +1,67 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "Engine/DeveloperSettingsBackedByCVars.h"
#include "UObject/SoftObjectPath.h"
#include "CommonLoadingScreenSettings.generated.h"
class UObject;
/**
* Settings for a loading screen system.
*/
UCLASS(config=Game, defaultconfig, meta=(DisplayName="Common Loading Screen"))
class UCommonLoadingScreenSettings : public UDeveloperSettingsBackedByCVars
{
GENERATED_BODY()
public:
UCommonLoadingScreenSettings();
public:
// The widget to load for the loading screen.
UPROPERTY(config, EditAnywhere, Category=Display, meta=(MetaClass="/Script/UMG.UserWidget"))
FSoftClassPath LoadingScreenWidget;
// The z-order of the loading screen widget in the viewport stack
UPROPERTY(config, EditAnywhere, Category=Display)
int32 LoadingScreenZOrder = 10000;
// How long to hold the loading screen up after other loading finishes (in seconds) to
// try to give texture streaming a chance to avoid blurriness
//
// Note: This is not normally applied in the editor for iteration time, but can be
// enabled via HoldLoadingScreenAdditionalSecsEvenInEditor
UPROPERTY(config, EditAnywhere, Category=Configuration, meta=(ForceUnits=s, ConsoleVariable="CommonLoadingScreen.HoldLoadingScreenAdditionalSecs"))
float HoldLoadingScreenAdditionalSecs = 2.0f;
// The interval in seconds beyond which the loading screen is considered permanently hung (if non-zero).
UPROPERTY(config, EditAnywhere, Category=Configuration, meta=(ForceUnits=s))
float LoadingScreenHeartbeatHangDuration = 0.0f;
// The interval in seconds between each log of what is keeping a loading screen up (if non-zero).
UPROPERTY(config, EditAnywhere, Category=Configuration, meta=(ForceUnits=s))
float LogLoadingScreenHeartbeatInterval = 5.0f;
// When true, the reason the loading screen is shown or hidden will be printed to the log every frame.
UPROPERTY(Transient, EditAnywhere, Category=Debugging, meta=(ConsoleVariable="CommonLoadingScreen.LogLoadingScreenReasonEveryFrame"))
bool LogLoadingScreenReasonEveryFrame = 0;
// Force the loading screen to be displayed (useful for debugging)
UPROPERTY(Transient, EditAnywhere, Category=Debugging, meta=(ConsoleVariable="CommonLoadingScreen.AlwaysShow"))
bool ForceLoadingScreenVisible = false;
// Should we apply the additional HoldLoadingScreenAdditionalSecs delay even in the editor
// (useful when iterating on loading screens)
UPROPERTY(Transient, EditAnywhere, Category=Debugging)
bool HoldLoadingScreenAdditionalSecsEvenInEditor = false;
// Should we apply the additional HoldLoadingScreenAdditionalSecs delay even in the editor
// (useful when iterating on loading screens)
UPROPERTY(config, EditAnywhere, Category=Configuration)
bool ForceTickLoadingScreenEvenInEditor = true;
};

View File

@ -0,0 +1,647 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "LoadingScreenManager.h"
#include "HAL/ThreadHeartBeat.h"
#include "Engine/GameInstance.h"
#include "Engine/GameViewportClient.h"
#include "Engine/Engine.h"
#include "GameFramework/GameStateBase.h"
#include "GameFramework/WorldSettings.h"
#include "Misc/CommandLine.h"
#include "Misc/ConfigCacheIni.h"
#include "LoadingProcessInterface.h"
#include "Framework/Application/IInputProcessor.h"
#include "Framework/Application/SlateApplication.h"
#include "PreLoadScreen.h"
#include "PreLoadScreenManager.h"
#include "ShaderPipelineCache.h"
#include "CommonLoadingScreenSettings.h"
//@TODO: Used as the placeholder widget in error cases, should probably create a wrapper that at least centers it/etc...
#include "Widgets/Images/SThrobber.h"
#include "Blueprint/UserWidget.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(LoadingScreenManager)
DECLARE_LOG_CATEGORY_EXTERN(LogLoadingScreen, Log, All);
DEFINE_LOG_CATEGORY(LogLoadingScreen);
//@TODO: Why can GetLocalPlayers() have nullptr entries? Can it really?
//@TODO: Test with PIE mode set to simulate and decide how much (if any) loading screen action should occur
//@TODO: Allow other things implementing ILoadingProcessInterface besides GameState/PlayerController (and owned components) to register as interested parties
//@TODO: ChangeMusicSettings (either here or using the LoadingScreenVisibilityChanged delegate)
//@TODO: Studio analytics (FireEvent_PIEFinishedLoading / tracking PIE startup time for regressions, either here or using the LoadingScreenVisibilityChanged delegate)
// Profiling category for loading screens
CSV_DEFINE_CATEGORY(LoadingScreen, true);
//////////////////////////////////////////////////////////////////////
bool ILoadingProcessInterface::ShouldShowLoadingScreen(UObject* TestObject, FString& OutReason)
{
if (TestObject != nullptr)
{
if (ILoadingProcessInterface* LoadObserver = Cast<ILoadingProcessInterface>(TestObject))
{
FString ObserverReason;
if (LoadObserver->ShouldShowLoadingScreen(/*out*/ ObserverReason))
{
if (ensureMsgf(!ObserverReason.IsEmpty(), TEXT("%s failed to set a reason why it wants to show the loading screen"), *GetPathNameSafe(TestObject)))
{
OutReason = ObserverReason;
}
return true;
}
}
}
return false;
}
//////////////////////////////////////////////////////////////////////
namespace LoadingScreenCVars
{
// CVars
static float HoldLoadingScreenAdditionalSecs = 2.0f;
static FAutoConsoleVariableRef CVarHoldLoadingScreenUpAtLeastThisLongInSecs(
TEXT("CommonLoadingScreen.HoldLoadingScreenAdditionalSecs"),
HoldLoadingScreenAdditionalSecs,
TEXT("How long to hold the loading screen up after other loading finishes (in seconds) to try to give texture streaming a chance to avoid blurriness"),
ECVF_Default | ECVF_Preview);
static bool LogLoadingScreenReasonEveryFrame = false;
static FAutoConsoleVariableRef CVarLogLoadingScreenReasonEveryFrame(
TEXT("CommonLoadingScreen.LogLoadingScreenReasonEveryFrame"),
LogLoadingScreenReasonEveryFrame,
TEXT("When true, the reason the loading screen is shown or hidden will be printed to the log every frame."),
ECVF_Default);
static bool ForceLoadingScreenVisible = false;
static FAutoConsoleVariableRef CVarForceLoadingScreenVisible(
TEXT("CommonLoadingScreen.AlwaysShow"),
ForceLoadingScreenVisible,
TEXT("Force the loading screen to show."),
ECVF_Default);
}
//////////////////////////////////////////////////////////////////////
// FLoadingScreenInputPreProcessor
// Input processor to throw in when loading screen is shown
// This will capture any inputs, so active menus under the loading screen will not interact
class FLoadingScreenInputPreProcessor : public IInputProcessor
{
public:
FLoadingScreenInputPreProcessor() { }
virtual ~FLoadingScreenInputPreProcessor() { }
bool CanEatInput() const
{
return !GIsEditor;
}
//~IInputProcess interface
virtual void Tick(const float DeltaTime, FSlateApplication& SlateApp, TSharedRef<ICursor> Cursor) override { }
virtual bool HandleKeyDownEvent(FSlateApplication& SlateApp, const FKeyEvent& InKeyEvent) override { return CanEatInput(); }
virtual bool HandleKeyUpEvent(FSlateApplication& SlateApp, const FKeyEvent& InKeyEvent) override { return CanEatInput(); }
virtual bool HandleAnalogInputEvent(FSlateApplication& SlateApp, const FAnalogInputEvent& InAnalogInputEvent) override { return CanEatInput(); }
virtual bool HandleMouseMoveEvent(FSlateApplication& SlateApp, const FPointerEvent& MouseEvent) override { return CanEatInput(); }
virtual bool HandleMouseButtonDownEvent(FSlateApplication& SlateApp, const FPointerEvent& MouseEvent) override { return CanEatInput(); }
virtual bool HandleMouseButtonUpEvent(FSlateApplication& SlateApp, const FPointerEvent& MouseEvent) override { return CanEatInput(); }
virtual bool HandleMouseButtonDoubleClickEvent(FSlateApplication& SlateApp, const FPointerEvent& MouseEvent) override { return CanEatInput(); }
virtual bool HandleMouseWheelOrGestureEvent(FSlateApplication& SlateApp, const FPointerEvent& InWheelEvent, const FPointerEvent* InGestureEvent) override { return CanEatInput(); }
virtual bool HandleMotionDetectedEvent(FSlateApplication& SlateApp, const FMotionEvent& MotionEvent) override { return CanEatInput(); }
//~End of IInputProcess interface
};
//////////////////////////////////////////////////////////////////////
// ULoadingScreenManager
void ULoadingScreenManager::Initialize(FSubsystemCollectionBase& Collection)
{
FCoreUObjectDelegates::PreLoadMapWithContext.AddUObject(this, &ThisClass::HandlePreLoadMap);
FCoreUObjectDelegates::PostLoadMapWithWorld.AddUObject(this, &ThisClass::HandlePostLoadMap);
const UGameInstance* LocalGameInstance = GetGameInstance();
check(LocalGameInstance);
}
void ULoadingScreenManager::Deinitialize()
{
StopBlockingInput();
RemoveWidgetFromViewport();
FCoreUObjectDelegates::PreLoadMap.RemoveAll(this);
FCoreUObjectDelegates::PostLoadMapWithWorld.RemoveAll(this);
// We are done, so do not attempt to tick us again
SetTickableTickType(ETickableTickType::Never);
}
bool ULoadingScreenManager::ShouldCreateSubsystem(UObject* Outer) const
{
// Only clients have loading screens
const UGameInstance* GameInstance = CastChecked<UGameInstance>(Outer);
const bool bIsServerWorld = GameInstance->IsDedicatedServerInstance();
return !bIsServerWorld;
}
void ULoadingScreenManager::Tick(float DeltaTime)
{
UpdateLoadingScreen();
TimeUntilNextLogHeartbeatSeconds = FMath::Max(TimeUntilNextLogHeartbeatSeconds - DeltaTime, 0.0);
}
ETickableTickType ULoadingScreenManager::GetTickableTickType() const
{
if (IsTemplate())
{
return ETickableTickType::Never;
}
return ETickableTickType::Conditional;
}
bool ULoadingScreenManager::IsTickable() const
{
// Don't tick if we don't have a game viewport client, this catches cases that ShouldCreateSubsystem does not
UGameInstance* GameInstance = GetGameInstance();
return (GameInstance && GameInstance->GetGameViewportClient());
}
TStatId ULoadingScreenManager::GetStatId() const
{
RETURN_QUICK_DECLARE_CYCLE_STAT(ULoadingScreenManager, STATGROUP_Tickables);
}
UWorld* ULoadingScreenManager::GetTickableGameObjectWorld() const
{
return GetGameInstance()->GetWorld();
}
void ULoadingScreenManager::RegisterLoadingProcessor(TScriptInterface<ILoadingProcessInterface> Interface)
{
ExternalLoadingProcessors.Add(Interface.GetObject());
}
void ULoadingScreenManager::UnregisterLoadingProcessor(TScriptInterface<ILoadingProcessInterface> Interface)
{
ExternalLoadingProcessors.Remove(Interface.GetObject());
}
void ULoadingScreenManager::HandlePreLoadMap(const FWorldContext& WorldContext, const FString& MapName)
{
if (WorldContext.OwningGameInstance == GetGameInstance())
{
bCurrentlyInLoadMap = true;
// Update the loading screen immediately if the engine is initialized
if (GEngine->IsInitialized())
{
UpdateLoadingScreen();
}
}
}
void ULoadingScreenManager::HandlePostLoadMap(UWorld* World)
{
if ((World != nullptr) && (World->GetGameInstance() == GetGameInstance()))
{
bCurrentlyInLoadMap = false;
}
}
void ULoadingScreenManager::UpdateLoadingScreen()
{
bool bLogLoadingScreenStatus = LoadingScreenCVars::LogLoadingScreenReasonEveryFrame;
if (ShouldShowLoadingScreen())
{
const UCommonLoadingScreenSettings* Settings = GetDefault<UCommonLoadingScreenSettings>();
// If we don't make it to the specified checkpoint in the given time will trigger the hang detector so we can better determine where progress stalled.
FThreadHeartBeat::Get().MonitorCheckpointStart(GetFName(), Settings->LoadingScreenHeartbeatHangDuration);
ShowLoadingScreen();
if ((Settings->LogLoadingScreenHeartbeatInterval > 0.0f) && (TimeUntilNextLogHeartbeatSeconds <= 0.0))
{
bLogLoadingScreenStatus = true;
TimeUntilNextLogHeartbeatSeconds = Settings->LogLoadingScreenHeartbeatInterval;
}
}
else
{
HideLoadingScreen();
FThreadHeartBeat::Get().MonitorCheckpointEnd(GetFName());
}
if (bLogLoadingScreenStatus)
{
UE_LOG(LogLoadingScreen, Log, TEXT("Loading screen showing: %d. Reason: %s"), bCurrentlyShowingLoadingScreen ? 1 : 0, *DebugReasonForShowingOrHidingLoadingScreen);
}
}
bool ULoadingScreenManager::CheckForAnyNeedToShowLoadingScreen()
{
// Start out with 'unknown' reason in case someone forgets to put a reason when changing this in the future.
DebugReasonForShowingOrHidingLoadingScreen = TEXT("Reason for Showing/Hiding LoadingScreen is unknown!");
const UGameInstance* LocalGameInstance = GetGameInstance();
if (LoadingScreenCVars::ForceLoadingScreenVisible)
{
DebugReasonForShowingOrHidingLoadingScreen = FString(TEXT("CommonLoadingScreen.AlwaysShow is true"));
return true;
}
const FWorldContext* Context = LocalGameInstance->GetWorldContext();
if (Context == nullptr)
{
// We don't have a world context right now... better show a loading screen
DebugReasonForShowingOrHidingLoadingScreen = FString(TEXT("The game instance has a null WorldContext"));
return true;
}
UWorld* World = Context->World();
if (World == nullptr)
{
DebugReasonForShowingOrHidingLoadingScreen = FString(TEXT("We have no world (FWorldContext's World() is null)"));
return true;
}
AGameStateBase* GameState = World->GetGameState<AGameStateBase>();
if (GameState == nullptr)
{
// The game state has not yet replicated.
DebugReasonForShowingOrHidingLoadingScreen = FString(TEXT("GameState hasn't yet replicated (it's null)"));
return true;
}
if (bCurrentlyInLoadMap)
{
// Show a loading screen if we are in LoadMap
DebugReasonForShowingOrHidingLoadingScreen = FString(TEXT("bCurrentlyInLoadMap is true"));
return true;
}
if (!Context->TravelURL.IsEmpty())
{
// Show a loading screen when pending travel
DebugReasonForShowingOrHidingLoadingScreen = FString(TEXT("We have pending travel (the TravelURL is not empty)"));
return true;
}
if (Context->PendingNetGame != nullptr)
{
// Connecting to another server
DebugReasonForShowingOrHidingLoadingScreen = FString(TEXT("We are connecting to another server (PendingNetGame != nullptr)"));
return true;
}
if (!World->HasBegunPlay())
{
DebugReasonForShowingOrHidingLoadingScreen = FString(TEXT("World hasn't begun play"));
return true;
}
if (World->IsInSeamlessTravel())
{
// Show a loading screen during seamless travel
DebugReasonForShowingOrHidingLoadingScreen = FString(TEXT("We are in seamless travel"));
return true;
}
// Ask the game state if it needs a loading screen
if (ILoadingProcessInterface::ShouldShowLoadingScreen(GameState, /*out*/ DebugReasonForShowingOrHidingLoadingScreen))
{
return true;
}
// Ask any game state components if they need a loading screen
for (UActorComponent* TestComponent : GameState->GetComponents())
{
if (ILoadingProcessInterface::ShouldShowLoadingScreen(TestComponent, /*out*/ DebugReasonForShowingOrHidingLoadingScreen))
{
return true;
}
}
// Ask any of the external loading processors that may have been registered. These might be actors or components
// that were registered by game code to tell us to keep the loading screen up while perhaps something finishes
// streaming in.
for (const TWeakInterfacePtr<ILoadingProcessInterface>& Processor : ExternalLoadingProcessors)
{
if (ILoadingProcessInterface::ShouldShowLoadingScreen(Processor.GetObject(), /*out*/ DebugReasonForShowingOrHidingLoadingScreen))
{
return true;
}
}
// Check each local player
bool bFoundAnyLocalPC = false;
bool bMissingAnyLocalPC = false;
for (ULocalPlayer* LP : LocalGameInstance->GetLocalPlayers())
{
if (LP != nullptr)
{
if (APlayerController* PC = LP->PlayerController)
{
bFoundAnyLocalPC = true;
// Ask the PC itself if it needs a loading screen
if (ILoadingProcessInterface::ShouldShowLoadingScreen(PC, /*out*/ DebugReasonForShowingOrHidingLoadingScreen))
{
return true;
}
// Ask any PC components if they need a loading screen
for (UActorComponent* TestComponent : PC->GetComponents())
{
if (ILoadingProcessInterface::ShouldShowLoadingScreen(TestComponent, /*out*/ DebugReasonForShowingOrHidingLoadingScreen))
{
return true;
}
}
}
else
{
bMissingAnyLocalPC = true;
}
}
}
UGameViewportClient* GameViewportClient = LocalGameInstance->GetGameViewportClient();
const bool bIsInSplitscreen = GameViewportClient->GetCurrentSplitscreenConfiguration() != ESplitScreenType::None;
// In splitscreen we need all player controllers to be present
if (bIsInSplitscreen && bMissingAnyLocalPC)
{
DebugReasonForShowingOrHidingLoadingScreen = FString(TEXT("At least one missing local player controller in splitscreen"));
return true;
}
// And in non-splitscreen we need at least one player controller to be present
if (!bIsInSplitscreen && !bFoundAnyLocalPC)
{
DebugReasonForShowingOrHidingLoadingScreen = FString(TEXT("Need at least one local player controller"));
return true;
}
// Victory! The loading screen can go away now
DebugReasonForShowingOrHidingLoadingScreen = TEXT("(nothing wants to show it anymore)");
return false;
}
bool ULoadingScreenManager::ShouldShowLoadingScreen()
{
const UCommonLoadingScreenSettings* Settings = GetDefault<UCommonLoadingScreenSettings>();
// Check debugging commands that force the state one way or another
#if !UE_BUILD_SHIPPING
static bool bCmdLineNoLoadingScreen = FParse::Param(FCommandLine::Get(), TEXT("NoLoadingScreen"));
if (bCmdLineNoLoadingScreen)
{
DebugReasonForShowingOrHidingLoadingScreen = FString(TEXT("CommandLine has 'NoLoadingScreen'"));
return false;
}
#endif
// Can't show a loading screen if there's no game viewport
UGameInstance* LocalGameInstance = GetGameInstance();
if (LocalGameInstance->GetGameViewportClient() == nullptr)
{
return false;
}
// Check for a need to show the loading screen
const bool bNeedToShowLoadingScreen = CheckForAnyNeedToShowLoadingScreen();
// Keep the loading screen up a bit longer if desired
bool bWantToForceShowLoadingScreen = false;
if (bNeedToShowLoadingScreen)
{
// Still need to show it
TimeLoadingScreenLastDismissed = -1.0;
}
else
{
// Don't *need* to show the screen anymore, but might still want to for a bit
const double CurrentTime = FPlatformTime::Seconds();
const bool bCanHoldLoadingScreen = (!GIsEditor || Settings->HoldLoadingScreenAdditionalSecsEvenInEditor);
const double HoldLoadingScreenAdditionalSecs = bCanHoldLoadingScreen ? LoadingScreenCVars::HoldLoadingScreenAdditionalSecs : 0.0;
if (TimeLoadingScreenLastDismissed < 0.0)
{
TimeLoadingScreenLastDismissed = CurrentTime;
}
const double TimeSinceScreenDismissed = CurrentTime - TimeLoadingScreenLastDismissed;
// hold for an extra X seconds, to cover up streaming
if ((HoldLoadingScreenAdditionalSecs > 0.0) && (TimeSinceScreenDismissed < HoldLoadingScreenAdditionalSecs))
{
// Make sure we're rendering the world at this point, so that textures will actually stream in
//@TODO: If bNeedToShowLoadingScreen bounces back true during this window, we won't turn this off again...
UGameViewportClient* GameViewportClient = GetGameInstance()->GetGameViewportClient();
GameViewportClient->bDisableWorldRendering = false;
DebugReasonForShowingOrHidingLoadingScreen = FString::Printf(TEXT("Keeping loading screen up for an additional %.2f seconds to allow texture streaming"), HoldLoadingScreenAdditionalSecs);
bWantToForceShowLoadingScreen = true;
}
}
return bNeedToShowLoadingScreen || bWantToForceShowLoadingScreen;
}
bool ULoadingScreenManager::IsShowingInitialLoadingScreen() const
{
FPreLoadScreenManager* PreLoadScreenManager = FPreLoadScreenManager::Get();
return (PreLoadScreenManager != nullptr) && PreLoadScreenManager->HasValidActivePreLoadScreen();
}
void ULoadingScreenManager::ShowLoadingScreen()
{
if (bCurrentlyShowingLoadingScreen)
{
return;
}
// Unable to show loading screen if the engine is still loading with its loading screen.
if (FPreLoadScreenManager::Get() && FPreLoadScreenManager::Get()->HasActivePreLoadScreenType(EPreLoadScreenTypes::EngineLoadingScreen))
{
return;
}
TimeLoadingScreenShown = FPlatformTime::Seconds();
bCurrentlyShowingLoadingScreen = true;
CSV_EVENT(LoadingScreen, TEXT("Show"));
const UCommonLoadingScreenSettings* Settings = GetDefault<UCommonLoadingScreenSettings>();
if (IsShowingInitialLoadingScreen())
{
UE_LOG(LogLoadingScreen, Log, TEXT("Showing loading screen when 'IsShowingInitialLoadingScreen()' is true."));
UE_LOG(LogLoadingScreen, Log, TEXT("%s"), *DebugReasonForShowingOrHidingLoadingScreen);
}
else
{
UE_LOG(LogLoadingScreen, Log, TEXT("Showing loading screen when 'IsShowingInitialLoadingScreen()' is false."));
UE_LOG(LogLoadingScreen, Log, TEXT("%s"), *DebugReasonForShowingOrHidingLoadingScreen);
UGameInstance* LocalGameInstance = GetGameInstance();
// Eat input while the loading screen is displayed
StartBlockingInput();
LoadingScreenVisibilityChanged.Broadcast(/*bIsVisible=*/ true);
// Create the loading screen widget
TSubclassOf<UUserWidget> LoadingScreenWidgetClass = Settings->LoadingScreenWidget.TryLoadClass<UUserWidget>();
if (UUserWidget* UserWidget = UUserWidget::CreateWidgetInstance(*LocalGameInstance, LoadingScreenWidgetClass, NAME_None))
{
LoadingScreenWidget = UserWidget->TakeWidget();
}
else
{
UE_LOG(LogLoadingScreen, Error, TEXT("Failed to load the loading screen widget %s, falling back to placeholder."), *Settings->LoadingScreenWidget.ToString());
LoadingScreenWidget = SNew(SThrobber);
}
// Add to the viewport at a high ZOrder to make sure it is on top of most things
UGameViewportClient* GameViewportClient = LocalGameInstance->GetGameViewportClient();
GameViewportClient->AddViewportWidgetContent(LoadingScreenWidget.ToSharedRef(), Settings->LoadingScreenZOrder);
ChangePerformanceSettings(/*bEnableLoadingScreen=*/ true);
if (!GIsEditor || Settings->ForceTickLoadingScreenEvenInEditor)
{
// Tick Slate to make sure the loading screen is displayed immediately
FSlateApplication::Get().Tick();
}
}
}
void ULoadingScreenManager::HideLoadingScreen()
{
if (!bCurrentlyShowingLoadingScreen)
{
return;
}
StopBlockingInput();
if (IsShowingInitialLoadingScreen())
{
UE_LOG(LogLoadingScreen, Log, TEXT("Hiding loading screen when 'IsShowingInitialLoadingScreen()' is true."));
UE_LOG(LogLoadingScreen, Log, TEXT("%s"), *DebugReasonForShowingOrHidingLoadingScreen);
}
else
{
UE_LOG(LogLoadingScreen, Log, TEXT("Hiding loading screen when 'IsShowingInitialLoadingScreen()' is false."));
UE_LOG(LogLoadingScreen, Log, TEXT("%s"), *DebugReasonForShowingOrHidingLoadingScreen);
UE_LOG(LogLoadingScreen, Log, TEXT("Garbage Collecting before dropping load screen"));
GEngine->ForceGarbageCollection(true);
RemoveWidgetFromViewport();
ChangePerformanceSettings(/*bEnableLoadingScreen=*/ false);
// Let observers know that the loading screen is done
LoadingScreenVisibilityChanged.Broadcast(/*bIsVisible=*/ false);
}
CSV_EVENT(LoadingScreen, TEXT("Hide"));
const double LoadingScreenDuration = FPlatformTime::Seconds() - TimeLoadingScreenShown;
UE_LOG(LogLoadingScreen, Log, TEXT("LoadingScreen was visible for %.2fs"), LoadingScreenDuration);
bCurrentlyShowingLoadingScreen = false;
}
void ULoadingScreenManager::RemoveWidgetFromViewport()
{
UGameInstance* LocalGameInstance = GetGameInstance();
if (LoadingScreenWidget.IsValid())
{
if (UGameViewportClient* GameViewportClient = LocalGameInstance->GetGameViewportClient())
{
GameViewportClient->RemoveViewportWidgetContent(LoadingScreenWidget.ToSharedRef());
}
LoadingScreenWidget.Reset();
}
}
void ULoadingScreenManager::StartBlockingInput()
{
if (!InputPreProcessor.IsValid())
{
InputPreProcessor = MakeShareable<FLoadingScreenInputPreProcessor>(new FLoadingScreenInputPreProcessor());
FSlateApplication::Get().RegisterInputPreProcessor(InputPreProcessor, 0);
}
}
void ULoadingScreenManager::StopBlockingInput()
{
if (InputPreProcessor.IsValid())
{
FSlateApplication::Get().UnregisterInputPreProcessor(InputPreProcessor);
InputPreProcessor.Reset();
}
}
void ULoadingScreenManager::ChangePerformanceSettings(bool bEnabingLoadingScreen)
{
UGameInstance* LocalGameInstance = GetGameInstance();
UGameViewportClient* GameViewportClient = LocalGameInstance->GetGameViewportClient();
FShaderPipelineCache::SetBatchMode(bEnabingLoadingScreen ? FShaderPipelineCache::BatchMode::Fast : FShaderPipelineCache::BatchMode::Background);
// Don't bother drawing the 3D world while we're loading
GameViewportClient->bDisableWorldRendering = bEnabingLoadingScreen;
// Make sure to prioritize streaming in levels if the loading screen is up
if (UWorld* ViewportWorld = GameViewportClient->GetWorld())
{
if (AWorldSettings* WorldSettings = ViewportWorld->GetWorldSettings(false, false))
{
WorldSettings->bHighPriorityLoadingLocal = bEnabingLoadingScreen;
}
}
if (bEnabingLoadingScreen)
{
// Set a new hang detector timeout multiplier when the loading screen is visible.
double HangDurationMultiplier;
if (!GConfig || !GConfig->GetDouble(TEXT("Core.System"), TEXT("LoadingScreenHangDurationMultiplier"), /*out*/ HangDurationMultiplier, GEngineIni))
{
HangDurationMultiplier = 1.0;
}
FThreadHeartBeat::Get().SetDurationMultiplier(HangDurationMultiplier);
// Do not report hitches while the loading screen is up
FGameThreadHitchHeartBeat::Get().SuspendHeartBeat();
}
else
{
// Restore the hang detector timeout when we hide the loading screen
FThreadHeartBeat::Get().SetDurationMultiplier(1.0);
// Resume reporting hitches now that the loading screen is down
FGameThreadHitchHeartBeat::Get().ResumeHeartBeat();
}
}

View File

@ -0,0 +1,30 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "LoadingProcessInterface.generated.h"
/** Interface for things that might cause loading to happen which requires a loading screen to be displayed */
UINTERFACE(BlueprintType)
class COMMONLOADINGSCREEN_API ULoadingProcessInterface : public UInterface
{
GENERATED_BODY()
};
class COMMONLOADINGSCREEN_API ILoadingProcessInterface
{
GENERATED_BODY()
public:
// Checks to see if this object implements the interface, and if so asks whether or not we should
// be currently showing a loading screen
static bool ShouldShowLoadingScreen(UObject* TestObject, FString& OutReason);
virtual bool ShouldShowLoadingScreen(FString& OutReason) const
{
return false;
}
};

View File

@ -0,0 +1,47 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "LoadingProcessTask.h"
#include "Engine/Engine.h"
#include "Engine/GameInstance.h"
#include "Engine/World.h"
#include "LoadingScreenManager.h"
#include "UObject/ScriptInterface.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(LoadingProcessTask)
/*static*/ ULoadingProcessTask* ULoadingProcessTask::CreateLoadingScreenProcessTask(UObject* WorldContextObject, const FString& ShowLoadingScreenReason)
{
UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::LogAndReturnNull);
UGameInstance* GameInstance = World ? World->GetGameInstance() : nullptr;
ULoadingScreenManager* LoadingScreenManager = GameInstance ? GameInstance->GetSubsystem<ULoadingScreenManager>() : nullptr;
if (LoadingScreenManager)
{
ULoadingProcessTask* NewLoadingTask = NewObject<ULoadingProcessTask>(LoadingScreenManager);
NewLoadingTask->SetShowLoadingScreenReason(ShowLoadingScreenReason);
LoadingScreenManager->RegisterLoadingProcessor(NewLoadingTask);
return NewLoadingTask;
}
return nullptr;
}
void ULoadingProcessTask::Unregister()
{
ULoadingScreenManager* LoadingScreenManager = Cast<ULoadingScreenManager>(GetOuter());
LoadingScreenManager->UnregisterLoadingProcessor(this);
}
void ULoadingProcessTask::SetShowLoadingScreenReason(const FString& InReason)
{
Reason = InReason;
}
bool ULoadingProcessTask::ShouldShowLoadingScreen(FString& OutReason) const
{
OutReason = Reason;
return true;
}

View File

@ -0,0 +1,33 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "LoadingProcessInterface.h"
#include "UObject/Object.h"
#include "LoadingProcessTask.generated.h"
struct FFrame;
UCLASS(BlueprintType)
class COMMONLOADINGSCREEN_API ULoadingProcessTask : public UObject, public ILoadingProcessInterface
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintCallable, meta=(WorldContext = "WorldContextObject"))
static ULoadingProcessTask* CreateLoadingScreenProcessTask(UObject* WorldContextObject, const FString& ShowLoadingScreenReason);
public:
ULoadingProcessTask() { }
UFUNCTION(BlueprintCallable)
void Unregister();
UFUNCTION(BlueprintCallable)
void SetShowLoadingScreenReason(const FString& InReason);
virtual bool ShouldShowLoadingScreen(FString& OutReason) const override;
FString Reason;
};

View File

@ -0,0 +1,127 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "Subsystems/GameInstanceSubsystem.h"
#include "Tickable.h"
#include "UObject/WeakInterfacePtr.h"
#include "LoadingScreenManager.generated.h"
template <typename InterfaceType> class TScriptInterface;
class FSubsystemCollectionBase;
class IInputProcessor;
class ILoadingProcessInterface;
class SWidget;
class UObject;
class UWorld;
struct FFrame;
struct FWorldContext;
/**
* Handles showing/hiding the loading screen
*/
UCLASS()
class COMMONLOADINGSCREEN_API ULoadingScreenManager : public UGameInstanceSubsystem, public FTickableGameObject
{
GENERATED_BODY()
public:
//~USubsystem interface
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
virtual void Deinitialize() override;
virtual bool ShouldCreateSubsystem(UObject* Outer) const override;
//~End of USubsystem interface
//~FTickableObjectBase interface
virtual void Tick(float DeltaTime) override;
virtual ETickableTickType GetTickableTickType() const override;
virtual bool IsTickable() const override;
virtual TStatId GetStatId() const override;
virtual UWorld* GetTickableGameObjectWorld() const override;
//~End of FTickableObjectBase interface
UFUNCTION(BlueprintCallable, Category=LoadingScreen)
FString GetDebugReasonForShowingOrHidingLoadingScreen() const
{
return DebugReasonForShowingOrHidingLoadingScreen;
}
/** Returns True when the loading screen is currently being shown */
bool GetLoadingScreenDisplayStatus() const
{
return bCurrentlyShowingLoadingScreen;
}
/** Called when the loading screen visibility changes */
DECLARE_MULTICAST_DELEGATE_OneParam(FOnLoadingScreenVisibilityChangedDelegate, bool);
FORCEINLINE FOnLoadingScreenVisibilityChangedDelegate& OnLoadingScreenVisibilityChangedDelegate() { return LoadingScreenVisibilityChanged; }
void RegisterLoadingProcessor(TScriptInterface<ILoadingProcessInterface> Interface);
void UnregisterLoadingProcessor(TScriptInterface<ILoadingProcessInterface> Interface);
private:
void HandlePreLoadMap(const FWorldContext& WorldContext, const FString& MapName);
void HandlePostLoadMap(UWorld* World);
/** Determines if we should show or hide the loading screen. Called every frame. */
void UpdateLoadingScreen();
/** Returns true if we need to be showing the loading screen. */
bool CheckForAnyNeedToShowLoadingScreen();
/** Returns true if we want to be showing the loading screen (if we need to or are artificially forcing it on for other reasons). */
bool ShouldShowLoadingScreen();
/** Returns true if we are in the initial loading flow before this screen should be used */
bool IsShowingInitialLoadingScreen() const;
/** Shows the loading screen. Sets up the loading screen widget on the viewport */
void ShowLoadingScreen();
/** Hides the loading screen. The loading screen widget will be destroyed */
void HideLoadingScreen();
/** Removes the widget from the viewport */
void RemoveWidgetFromViewport();
/** Prevents input from being used in-game while the loading screen is visible */
void StartBlockingInput();
/** Resumes in-game input, if blocking */
void StopBlockingInput();
void ChangePerformanceSettings(bool bEnabingLoadingScreen);
private:
/** Delegate broadcast when the loading screen visibility changes */
FOnLoadingScreenVisibilityChangedDelegate LoadingScreenVisibilityChanged;
/** A reference to the loading screen widget we are displaying (if any) */
TSharedPtr<SWidget> LoadingScreenWidget;
/** Input processor to eat all input while the loading screen is shown */
TSharedPtr<IInputProcessor> InputPreProcessor;
/** External loading processors, components maybe actors that delay the loading. */
TArray<TWeakInterfacePtr<ILoadingProcessInterface>> ExternalLoadingProcessors;
/** The reason why the loading screen is up (or not) */
FString DebugReasonForShowingOrHidingLoadingScreen;
/** The time when we started showing the loading screen */
double TimeLoadingScreenShown = 0.0;
/** The time the loading screen most recently wanted to be dismissed (might still be up due to a min display duration requirement) **/
double TimeLoadingScreenLastDismissed = -1.0;
/** The time until the next log for why the loading screen is still up */
double TimeUntilNextLogHeartbeatSeconds = 0.0;
/** True when we are between PreLoadMap and PostLoadMap */
bool bCurrentlyInLoadMap = false;
/** True when the loading screen is currently being shown */
bool bCurrentlyShowingLoadingScreen = false;
};

View File

@ -0,0 +1,55 @@
// Copyright Epic Games, Inc. All Rights Reserved.
using UnrealBuildTool;
public class CommonStartupLoadingScreen : ModuleRules
{
public CommonStartupLoadingScreen(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs;
PublicIncludePaths.AddRange(
new string[] {
// ... add public include paths required here ...
}
);
PrivateIncludePaths.AddRange(
new string[] {
// ... add other private include paths required here ...
}
);
PublicDependencyModuleNames.AddRange(
new string[]
{
"Core",
// ... add other public dependencies that you statically link with here ...
}
);
PrivateDependencyModuleNames.AddRange(
new string[]
{
"CoreUObject",
"Engine",
"Slate",
"SlateCore",
"MoviePlayer",
"PreLoadScreen",
"DeveloperSettings"
}
);
DynamicallyLoadedModuleNames.AddRange(
new string[]
{
// ... add any modules that your module loads dynamically here ...
}
);
}
}

View File

@ -0,0 +1,18 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "CommonPreLoadScreen.h"
#include "Misc/App.h"
#include "SCommonPreLoadingScreenWidget.h"
#define LOCTEXT_NAMESPACE "CommonPreLoadingScreen"
void FCommonPreLoadScreen::Init()
{
if (!GIsEditor && FApp::CanEverRender())
{
EngineLoadingWidget = SNew(SCommonPreLoadingScreenWidget);
}
}
#undef LOCTEXT_NAMESPACE

View File

@ -0,0 +1,20 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "PreLoadScreenBase.h"
class SWidget;
class FCommonPreLoadScreen : public FPreLoadScreenBase
{
public:
/*** IPreLoadScreen Implementation ***/
virtual void Init() override;
virtual EPreLoadScreenTypes GetPreLoadScreenType() const override { return EPreLoadScreenTypes::EngineLoadingScreen; }
virtual TSharedPtr<SWidget> GetWidget() override { return EngineLoadingWidget; }
private:
TSharedPtr<SWidget> EngineLoadingWidget;
};

View File

@ -0,0 +1,62 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "CommonPreLoadScreen.h"
#include "Misc/App.h"
#include "Modules/ModuleManager.h"
#include "PreLoadScreenManager.h"
#define LOCTEXT_NAMESPACE "FCommonLoadingScreenModule"
class FCommonStartupLoadingScreenModule : public IModuleInterface
{
public:
/** IModuleInterface implementation */
virtual void StartupModule() override;
virtual void ShutdownModule() override;
bool IsGameModule() const override;
private:
void OnPreLoadScreenManagerCleanUp();
TSharedPtr<FCommonPreLoadScreen> PreLoadingScreen;
};
void FCommonStartupLoadingScreenModule::StartupModule()
{
// No need to load these assets on dedicated servers.
// Still want to load them in commandlets so cook catches them
if (!IsRunningDedicatedServer())
{
PreLoadingScreen = MakeShared<FCommonPreLoadScreen>();
PreLoadingScreen->Init();
if (!GIsEditor && FApp::CanEverRender() && FPreLoadScreenManager::Get())
{
FPreLoadScreenManager::Get()->RegisterPreLoadScreen(PreLoadingScreen);
FPreLoadScreenManager::Get()->OnPreLoadScreenManagerCleanUp.AddRaw(this, &FCommonStartupLoadingScreenModule::OnPreLoadScreenManagerCleanUp);
}
}
}
void FCommonStartupLoadingScreenModule::OnPreLoadScreenManagerCleanUp()
{
//Once the PreLoadScreenManager is cleaning up, we can get rid of all our resources too
PreLoadingScreen.Reset();
ShutdownModule();
}
void FCommonStartupLoadingScreenModule::ShutdownModule()
{
}
bool FCommonStartupLoadingScreenModule::IsGameModule() const
{
return true;
}
#undef LOCTEXT_NAMESPACE
IMPLEMENT_MODULE(FCommonStartupLoadingScreenModule, CommonStartupLoadingScreen)

View File

@ -0,0 +1,32 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "SCommonPreLoadingScreenWidget.h"
#include "Widgets/Layout/SBorder.h"
class FReferenceCollector;
#define LOCTEXT_NAMESPACE "SCommonPreLoadingScreenWidget"
void SCommonPreLoadingScreenWidget::Construct(const FArguments& InArgs)
{
ChildSlot
[
SNew(SBorder)
.BorderImage(FCoreStyle::Get().GetBrush("WhiteBrush"))
.BorderBackgroundColor(FLinearColor::Black)
.Padding(0)
];
}
void SCommonPreLoadingScreenWidget::AddReferencedObjects(FReferenceCollector& Collector)
{
//WidgetAssets.AddReferencedObjects(Collector);
}
FString SCommonPreLoadingScreenWidget::GetReferencerName() const
{
return TEXT("SCommonPreLoadingScreenWidget");
}
#undef LOCTEXT_NAMESPACE

View File

@ -0,0 +1,26 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "UObject/GCObject.h"
#include "Widgets/Accessibility/SlateWidgetAccessibleTypes.h"
#include "Widgets/SCompoundWidget.h"
class FReferenceCollector;
class SCommonPreLoadingScreenWidget : public SCompoundWidget, public FGCObject
{
public:
SLATE_BEGIN_ARGS(SCommonPreLoadingScreenWidget) {}
SLATE_END_ARGS()
void Construct(const FArguments& InArgs);
//~ Begin FGCObject interface
virtual void AddReferencedObjects(FReferenceCollector& Collector) override;
virtual FString GetReferencerName() const override;
//~ End FGCObject interface
private:
};