301 lines
11 KiB
C++
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();
|
|
}
|
|
}
|
|
|