Loading screen overlay + camera fade on sublevel load

- Add loading overlay (black background + "Loading..." text) behind UI panels
- Overlay starts visible by default, hidden in NativeConstruct if no sublevel
- CameraFade to black before sublevel load, with deferred fallback for BeginPlay
- 2s delay after OnLevelShown before starting 2s fade-in from black
- ShowLoadingScreen helper on PlayerController via HUD->MainWidget

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-14 14:26:43 +02:00
parent 5affbc7d0d
commit c04bc2149b
4 changed files with 108 additions and 0 deletions

View File

@@ -11,6 +11,9 @@
#include "Engine/LevelStreamingDynamic.h" #include "Engine/LevelStreamingDynamic.h"
#include "Engine/PostProcessVolume.h" #include "Engine/PostProcessVolume.h"
#include "PS_Editor_CameraStart.h" #include "PS_Editor_CameraStart.h"
#include "PS_Editor_HUD.h"
#include "PS_Editor_MainWidget.h"
#include "Camera/PlayerCameraManager.h"
#include "PS_Editor_GameInstanceSubsystem.h" #include "PS_Editor_GameInstanceSubsystem.h"
#include "GameFramework/GameModeBase.h" #include "GameFramework/GameModeBase.h"
#include "GameFramework/GameStateBase.h" #include "GameFramework/GameStateBase.h"
@@ -136,6 +139,14 @@ void APS_Editor_PlayerController::Tick(float DeltaTime)
{ {
Super::Tick(DeltaTime); Super::Tick(DeltaTime);
// Deferred fade-to-black: CameraManager wasn't ready at BeginPlay
if (bPendingFadeToBlack && PlayerCameraManager)
{
PlayerCameraManager->StartCameraFade(0.0f, 1.0f, 0.0f, FLinearColor::Black, true, true);
bPendingFadeToBlack = false;
ShowLoadingScreen(true);
}
if (SpawnManager) if (SpawnManager)
{ {
SpawnManager->TickEditorMode(DeltaTime); SpawnManager->TickEditorMode(DeltaTime);
@@ -253,6 +264,17 @@ void APS_Editor_PlayerController::CancelPointPlacement()
FinishPointPlacement(); FinishPointPlacement();
} }
void APS_Editor_PlayerController::ShowLoadingScreen(bool bShow)
{
if (APS_Editor_HUD* EditorHUD = Cast<APS_Editor_HUD>(GetHUD()))
{
if (UPS_Editor_MainWidget* Widget = EditorHUD->GetMainWidget())
{
Widget->ShowLoadingScreen(bShow);
}
}
}
void APS_Editor_PlayerController::LoadBaseLevelAsSublevel(const FString& LevelName, const FString& MapPath) void APS_Editor_PlayerController::LoadBaseLevelAsSublevel(const FString& LevelName, const FString& MapPath)
{ {
// Unload previous sublevel first // Unload previous sublevel first
@@ -294,6 +316,19 @@ void APS_Editor_PlayerController::LoadBaseLevelAsSublevel(const FString& LevelNa
return; return;
} }
// Fade to black + show loading overlay
bWaitingForSublevelFadeIn = true;
if (PlayerCameraManager)
{
PlayerCameraManager->StartCameraFade(0.0f, 1.0f, 0.0f, FLinearColor::Black, true, true);
bPendingFadeToBlack = false;
}
else
{
bPendingFadeToBlack = true;
}
ShowLoadingScreen(true);
// Use MapPath if provided, otherwise fall back to LevelName (legacy behavior) // Use MapPath if provided, otherwise fall back to LevelName (legacy behavior)
const FString& LevelToLoad = MapPath.IsEmpty() ? LevelName : MapPath; const FString& LevelToLoad = MapPath.IsEmpty() ? LevelName : MapPath;
@@ -362,6 +397,21 @@ void APS_Editor_PlayerController::DisableSublevelPostProcessMaterials()
*CameraStart->GetActorLocation().ToString()); *CameraStart->GetActorLocation().ToString());
} }
} }
// Delay 2s then fade in from black over 2 seconds
if (bWaitingForSublevelFadeIn)
{
bWaitingForSublevelFadeIn = false;
FTimerHandle FadeTimer;
GetWorldTimerManager().SetTimer(FadeTimer, [this]()
{
ShowLoadingScreen(false);
if (PlayerCameraManager)
{
PlayerCameraManager->StartCameraFade(1.0f, 0.0f, 2.0f, FLinearColor::Black, false, false);
}
}, 2.0f, false);
}
} }
void APS_Editor_PlayerController::HandleEditorClick(FVector2D ScreenPos, bool bAdditive) void APS_Editor_PlayerController::HandleEditorClick(FVector2D ScreenPos, bool bAdditive)

View File

@@ -136,9 +136,18 @@ private:
UPROPERTY() UPROPERTY()
TObjectPtr<ULevelStreamingDynamic> CurrentSublevel; TObjectPtr<ULevelStreamingDynamic> CurrentSublevel;
/** True when a sublevel is loading and needs fade-in when ready. */
bool bWaitingForSublevelFadeIn = false;
/** True if we need to apply the fade-to-black once CameraManager is available. */
bool bPendingFadeToBlack = false;
/** Disable custom post-process materials in the loaded sublevel to avoid stencil conflicts. */ /** Disable custom post-process materials in the loaded sublevel to avoid stencil conflicts. */
UFUNCTION() UFUNCTION()
void DisableSublevelPostProcessMaterials(); void DisableSublevelPostProcessMaterials();
/** Show/hide loading screen overlay on the HUD widget. */
void ShowLoadingScreen(bool bShow);
void HandleEditorClick(FVector2D ScreenPos, bool bAdditive); void HandleEditorClick(FVector2D ScreenPos, bool bAdditive);
}; };

View File

@@ -45,6 +45,27 @@ void UPS_Editor_MainWidget::SetSceneSerializer(UPS_Editor_SceneSerializer* InSer
TSharedRef<SWidget> UPS_Editor_MainWidget::RebuildWidget() TSharedRef<SWidget> UPS_Editor_MainWidget::RebuildWidget()
{ {
return SNew(SOverlay) return SNew(SOverlay)
// Loading screen (behind everything — first slot = bottom layer)
+ SOverlay::Slot()
.HAlign(HAlign_Fill)
.VAlign(VAlign_Fill)
[
SAssignNew(LoadingOverlay, SBorder)
.Visibility(EVisibility::Visible)
.BorderImage(FCoreStyle::Get().GetBrush("WhiteBrush"))
.BorderBackgroundColor(FLinearColor(0.0f, 0.0f, 0.0f, 1.0f))
[
SNew(SBox)
.HAlign(HAlign_Center)
.VAlign(VAlign_Center)
[
SNew(STextBlock)
.Text(FText::FromString(TEXT("Loading...")))
.Font(FCoreStyle::GetDefaultFontStyle("Bold", 18))
.ColorAndOpacity(FLinearColor(0.7f, 0.7f, 0.7f, 1.0f))
]
]
]
// Top-left: Title + Toolbar // Top-left: Title + Toolbar
+ SOverlay::Slot() + SOverlay::Slot()
.HAlign(HAlign_Left) .HAlign(HAlign_Left)
@@ -999,6 +1020,19 @@ void UPS_Editor_MainWidget::NativeConstruct()
} }
PopulateSceneList(); PopulateSceneList();
// Hide loading overlay if no sublevel is being loaded
if (APS_Editor_PlayerController* PC = Cast<APS_Editor_PlayerController>(GetOwningPlayer()))
{
if (PC->CurrentBaseLevel.IsEmpty())
{
ShowLoadingScreen(false);
}
}
else
{
ShowLoadingScreen(false);
}
} }
void UPS_Editor_MainWidget::PopulateSceneList() void UPS_Editor_MainWidget::PopulateSceneList()
@@ -2184,6 +2218,14 @@ void UPS_Editor_MainWidget::OnModeButtonClicked(EPS_Editor_TransformMode Mode)
} }
} }
void UPS_Editor_MainWidget::ShowLoadingScreen(bool bShow)
{
if (LoadingOverlay.IsValid())
{
LoadingOverlay->SetVisibility(bShow ? EVisibility::Visible : EVisibility::Collapsed);
}
}
void UPS_Editor_MainWidget::OnCloseClicked() void UPS_Editor_MainWidget::OnCloseClicked()
{ {
if (CloseConfirmOverlay.IsValid()) if (CloseConfirmOverlay.IsValid())

View File

@@ -110,6 +110,13 @@ private:
bool bSceneDirty = false; bool bSceneDirty = false;
int32 LastKnownSpawnedCount = 0; int32 LastKnownSpawnedCount = 0;
// Loading screen
TSharedPtr<SWidget> LoadingOverlay;
public:
/** Show/hide the loading overlay. */
void ShowLoadingScreen(bool bShow);
// ---- Helpers ---- // ---- Helpers ----
TSharedRef<SWidget> BuildToolbar(); TSharedRef<SWidget> BuildToolbar();
TSharedRef<SWidget> BuildPropertiesPanel(); TSharedRef<SWidget> BuildPropertiesPanel();