301 lines
11 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "LoadingScreenManager.h"
#include "HAL/ThreadHeartBeat.h"
#include "ProfilingDebugging/CsvProfiler.h"
#include "LoadingScreenWidget.h"
#include "Engine/GameInstance.h"
#include "Engine/GameViewportClient.h"
#include "Engine/World.h"
#include "Engine/Engine.h"
#include "Engine/LocalPlayer.h"
#include "GameFramework/GameStateBase.h"
#include "GameFramework/WorldSettings.h"
#include "Misc/ConfigCacheIni.h"
#include "Framework/Application/IInputProcessor.h"
#include "Framework/Application/SlateApplication.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;
}
//////////////////////////////////////////////////////////////////////
// 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 this->CanEatInput(); }
virtual bool HandleKeyUpEvent(FSlateApplication& SlateApp, const FKeyEvent& InKeyEvent) override { return this->CanEatInput(); }
virtual bool HandleAnalogInputEvent(FSlateApplication& SlateApp, const FAnalogInputEvent& InAnalogInputEvent) override { return this->CanEatInput(); }
virtual bool HandleMouseMoveEvent(FSlateApplication& SlateApp, const FPointerEvent& MouseEvent) override { return this->CanEatInput(); }
virtual bool HandleMouseButtonDownEvent(FSlateApplication& SlateApp, const FPointerEvent& MouseEvent) override { return this->CanEatInput(); }
virtual bool HandleMouseButtonUpEvent(FSlateApplication& SlateApp, const FPointerEvent& MouseEvent) override { return this->CanEatInput(); }
virtual bool HandleMouseButtonDoubleClickEvent(FSlateApplication& SlateApp, const FPointerEvent& MouseEvent) override { return this->CanEatInput(); }
virtual bool HandleMouseWheelOrGestureEvent(FSlateApplication& SlateApp, const FPointerEvent& InWheelEvent, const FPointerEvent* InGestureEvent) override { return this->CanEatInput(); }
virtual bool HandleMotionDetectedEvent(FSlateApplication& SlateApp, const FMotionEvent& MotionEvent) override { return this->CanEatInput(); }
//~End of IInputProcess interface
};
//////////////////////////////////////////////////////////////////////
// ULoadingScreenManager
void ULoadingScreenManager::Initialize(FSubsystemCollectionBase &Collection)
{
const UGameInstance *LocalGameInstance = this->GetGameInstance();
check(LocalGameInstance);
}
void ULoadingScreenManager::Deinitialize()
{
this->StopBlockingInput();
this->RemoveWidgetFromViewport();
}
bool ULoadingScreenManager::ShouldCreateSubsystem(UObject *Outer) const
{
// Only clients have loading screens
const UGameInstance *GameInstance = CastChecked<UGameInstance>(Outer);
const bool bIsServerWorld = GameInstance->IsDedicatedServerInstance();
return !bIsServerWorld;
}
ULoadingScreenWidget *ULoadingScreenManager::ShowLoadingScreen()
{
if (this->bCurrentlyShowingLoadingScreen)
{
return nullptr;
}
// Unable to show loading screen if the engine is still loading with its loading screen.
if (FPreLoadScreenManager::Get() && FPreLoadScreenManager::Get()->HasActivePreLoadScreenType(EPreLoadScreenTypes::EngineLoadingScreen))
{
return nullptr;
}
this->TimeLoadingScreenShown = FPlatformTime::Seconds();
this->bCurrentlyShowingLoadingScreen = true;
CSV_EVENT(LoadingScreen, TEXT("Show"));
// Eat input while the loading screen is displayed
this->StartBlockingInput();
this->LoadingScreenVisibilityChanged.Broadcast(/*bIsVisible=*/ true);
// Create the loading screen widget
const UCommonLoadingScreenSettings *Settings = GetDefault<UCommonLoadingScreenSettings>();
UGameInstance *LocalGameInstance = this->GetGameInstance();
TSubclassOf<ULoadingScreenWidget> LoadingScreenWidgetClass = Settings->LoadingScreenWidget.LoadSynchronous();
if (this->LoadingScreenUMGWidget = CreateWidget<ULoadingScreenWidget>(LocalGameInstance, LoadingScreenWidgetClass, NAME_None))
{
this->LoadingScreenWidget = this->LoadingScreenUMGWidget->TakeWidget();
}
else
{
UE_LOG(LogLoadingScreen, Error, TEXT("Failed to load the loading screen widget %s, falling back to placeholder."), *Settings->LoadingScreenWidget.ToString());
this->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(this->LoadingScreenWidget.ToSharedRef(), Settings->LoadingScreenZOrder);
this->ChangePerformanceSettings(/*bEnableLoadingScreen=*/ true);
if (!GIsEditor || Settings->ForceTickLoadingScreenEvenInEditor)
{
// Tick Slate to make sure the loading screen is displayed immediately
FSlateApplication::Get().Tick();
}
return this->LoadingScreenUMGWidget;
}
void ULoadingScreenManager::HideLoadingScreen()
{
const float HoldTime = GetDefault<UCommonLoadingScreenSettings>()->HoldLoadingScreenAdditionalSecs;
if (FMath::IsNearlyZero(HoldTime))
{
this->HideLoadingScreen_Private();
}
else
{
this->GetWorld()->GetTimerManager().SetTimer(
this->HideLoadingScreenTimerHandle,
this, &ULoadingScreenManager::HideLoadingScreen_Private,
HoldTime
);
}
}
void ULoadingScreenManager::HideLoadingScreen_Private()
{
if (this->bCurrentlyShowingLoadingScreen)
{
this->ChangePerformanceSettings(/*bEnableLoadingScreen=*/ false);
this->LoadingScreenUMGWidget->OnLoadScreenClosed.BindUObject(this, &ULoadingScreenManager::RemoveLoadingScreen);
this->LoadingScreenUMGWidget->BeginFadeOut();
}
}
void ULoadingScreenManager::RemoveLoadingScreen()
{
if (!this->bCurrentlyShowingLoadingScreen)
{
return;
}
this->StopBlockingInput();
this->RemoveWidgetFromViewport();
this->LoadingScreenUMGWidget->OnLoadScreenOpened.Unbind();
this->LoadingScreenUMGWidget->OnLoadScreenClosed.Unbind();
// Let observers know that the loading screen is done
this->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);
this->bCurrentlyShowingLoadingScreen = false;
}
void ULoadingScreenManager::RemoveWidgetFromViewport()
{
UGameInstance *LocalGameInstance = GetGameInstance();
if (this->LoadingScreenWidget.IsValid())
{
if (UGameViewportClient *GameViewportClient = LocalGameInstance->GetGameViewportClient())
{
GameViewportClient->RemoveViewportWidgetContent(this->LoadingScreenWidget.ToSharedRef());
}
this->LoadingScreenWidget.Reset();
}
}
void ULoadingScreenManager::StartBlockingInput()
{
if (!this->InputPreProcessor.IsValid())
{
this->InputPreProcessor = MakeShareable<FLoadingScreenInputPreProcessor>(new FLoadingScreenInputPreProcessor());
FSlateApplication::Get().RegisterInputPreProcessor(this->InputPreProcessor, 0);
}
}
void ULoadingScreenManager::StopBlockingInput()
{
if (this->InputPreProcessor.IsValid())
{
FSlateApplication::Get().UnregisterInputPreProcessor(this->InputPreProcessor);
this->InputPreProcessor.Reset();
}
}
void ULoadingScreenManager::ChangePerformanceSettings(bool bEnabingLoadingScreen)
{
UGameInstance *LocalGameInstance = this->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();
}
}