(Feat) Fix Fade Loading

This commit is contained in:
2026-08-03 21:10:45 +02:00
parent 7595df6862
commit 1456ed91c7
11 changed files with 925 additions and 28 deletions
@@ -8,6 +8,7 @@
#include "EnhancedInputComponent.h"
#include "FpsPlayer.h"
#include "GameFramework/GameModeBase.h"
#include "SurvivalGameInstance.h"
#include "SurvivalStatsComponent.h"
#include "HotbarWidget.h"
#include "InputActionValue.h"
@@ -83,6 +84,7 @@ void AFpsPlayerController::BeginPlay()
{
Super::BeginPlay();
// Le mode d'input vit sur le VIEWPORT, pas sur le controller. OpenLevel
// detruit le controller du menu principal mais pas la fenetre : son
// GameAndUI et son curseur visible restent en place, et la camera ne
@@ -115,16 +117,135 @@ void AFpsPlayerController::BeginPlay()
// bQuittingToMainMenu qui distingue le fondu de sortie du reste.
ScreenFade->OnFadeFinished.AddDynamic(this, &AFpsPlayerController::HandleFadeFinished);
if (bFadeInOnStart)
// Une invitation peut tomber en pleine partie : sans ce fondu, l'ecran
// sauterait directement dans le monde de l'autre hote.
if (USessionSubsystem* Sessions = GetGameInstance() ? GetGameInstance()->GetSubsystem<USessionSubsystem>() : nullptr)
{
ScreenFade->FadeIn(StartupFadeDuration);
Sessions->OnTravelPending.AddDynamic(this, &AFpsPlayerController::HandleSessionTravelPending);
}
// IsLocalController est INDISPENSABLE ici : sur la machine de l'hote, chaque
// joueur distant a AUSSI un controller cote serveur. Sans cette garde, le
// controller d'un ami qui se connecte irait poser un fondu et rallumer le
// rendu du viewport de l'HOTE, a un moment totalement arbitraire.
if (bFadeInOnStart && IsLocalController())
{
// On NOIRCIT et on attend que la camera soit sur notre corps, au lieu
// d'eclaircir tout de suite. Un client demarre sur un SpectatorPawn ne
// au centre du monde -- sous le terrain -- et y reste encore un instant
// apres l'acquittement de la possession. C'est TryStartupFadeIn qui
// decidera du bon moment.
ScreenFade->SnapToBlack();
bStartupFadePending = true;
// Filet : si la possession n'arrivait jamais, l'ecran resterait noir a
// vie et le joueur croirait le jeu plante.
// Le filet appelle CompleteStartupFadeIn et non TryStartupFadeIn : sans
// ca il retomberait dans la boucle de reessai et l'ecran resterait noir
// pour toujours.
GetWorldTimerManager().SetTimer(
StartupFadeTimeoutHandle, this, &AFpsPlayerController::CompleteStartupFadeIn,
FMath::Max(StartupFadeTimeout, 1.f), /*bLoop=*/false);
// L'hote possede deja son pawn ET sa camera est dessus : il n'attendra
// rien. Le test reel est dans TryStartupFadeIn.
TryStartupFadeIn();
}
else if (IsLocalController())
{
// Mode sans fondu : personne ne rallumera le rendu, il faut le faire ici
// sous peine d'ecran noir jusqu'au filet de securite.
if (USurvivalGameInstance* SurvivalGameInstance = GetGameInstance<USurvivalGameInstance>())
{
SurvivalGameInstance->ResumeWorldRendering();
}
}
}
void AFpsPlayerController::TryStartupFadeIn()
{
if (!bStartupFadePending)
{
return;
}
// Un controller distant vu depuis le serveur n'a pas d'ecran a eclaircir, et
// surtout pas celui de l'hote.
if (!IsLocalController())
{
return;
}
// La camera doit deja regarder par NOS yeux. Un client demarre sur un
// SpectatorPawn ne au centre du monde -- sous le terrain -- et y reste
// encore un instant apres l'acquittement de la possession. Eclaircir a cet
// instant montre exactement ca.
const APawn* OwnPawn = GetPawn();
if (!OwnPawn || GetViewTarget() != OwnPawn)
{
GetWorldTimerManager().SetTimer(
StartupFadeRetryHandle, this, &AFpsPlayerController::TryStartupFadeIn,
StartupFadeRetry, /*bLoop=*/false);
return;
}
CompleteStartupFadeIn();
}
void AFpsPlayerController::CompleteStartupFadeIn()
{
if (!bStartupFadePending)
{
return;
}
bStartupFadePending = false;
GetWorldTimerManager().ClearTimer(StartupFadeTimeoutHandle);
GetWorldTimerManager().ClearTimer(StartupFadeRetryHandle);
// Le rendu du monde est suspendu depuis le chargement de la map : on le
// rend AVANT le fondu, alors que le voile est encore parfaitement noir.
// L'ordre inverse laisserait passer les images qu'on cherche a cacher.
USurvivalGameInstance* SurvivalGameInstance = GetGameInstance<USurvivalGameInstance>();
if (!SurvivalGameInstance)
{
StartStartupFade();
return;
}
SurvivalGameInstance->ResumeWorldRendering();
// On laisse le monde rendre quelques images DERRIERE le voile noir avant de
// l'eclaircir : c'est ce qui donne a Nanite, aux ombres virtuelles et a
// Lumen le temps de converger sans que personne le voie.
SurvivalGameInstance->BeginSceneWarmup(
FSimpleDelegate::CreateUObject(this, &AFpsPlayerController::StartStartupFade));
}
void AFpsPlayerController::StartStartupFade()
{
ScreenFade->FadeIn(StartupFadeDuration);
}
void AFpsPlayerController::OnPossess(APawn* InPawn)
{
Super::OnPossess(InPawn);
SetupForPossessedPawn(InPawn);
}
void AFpsPlayerController::AcknowledgePossession(APawn* P)
{
Super::AcknowledgePossession(P);
// Le chemin CLIENT. Sans lui, un joueur qui rejoint n'a aucun HUD : le
// serveur a bien appele OnPossess, mais chez lui, pas chez le client.
SetupForPossessedPawn(P);
}
void AFpsPlayerController::SetupForPossessedPawn(APawn* InPawn)
{
// On cree les widgets ici et pas dans BeginPlay : a ce stade le pawn est
// garanti possede, et le prompt comme l'inventaire ont besoin de lui pour
// trouver leurs composants des leur NativeConstruct.
@@ -132,7 +253,6 @@ void AFpsPlayerController::OnPossess(APawn* InPawn)
// Apres un respawn les widgets existent deja, mais pointent vers les
// composants du cadavre. On les rebranche sur le nouveau pawn.
if (Crosshair) { /* pas d'abonnement */ }
if (InteractionPrompt) { InteractionPrompt->BindToOwningPawn(); }
if (Hotbar) { Hotbar->BindToOwningPawn(); }
if (InventoryScreen) { InventoryScreen->BindToOwningPawn(); }
@@ -142,9 +262,16 @@ void AFpsPlayerController::OnPossess(APawn* InPawn)
{
if (USurvivalStatsComponent* Stats = PlayerPawn->GetSurvivalStats())
{
Stats->OnDied.AddDynamic(this, &AFpsPlayerController::HandlePawnDied);
// AddUnique et non Add : chez l'hote, OnPossess ET
// AcknowledgePossession passent tous les deux ici. Un double
// abonnement declencherait deux fondus de mort et deux respawns.
Stats->OnDied.AddUniqueDynamic(this, &AFpsPlayerController::HandlePawnDied);
}
}
// Le corps est la : on peut enfin montrer le monde. Sans effet si le fondu
// d'arrivee a deja eu lieu, ce qui est le cas a chaque respawn.
TryStartupFadeIn();
}
void AFpsPlayerController::HandlePawnDied()
@@ -1091,8 +1218,88 @@ void AFpsPlayerController::RequestQuitToMainMenu()
void AFpsPlayerController::HandleQuitToMenuConfirmed()
{
bQuitToMenuDialogOpen = false;
// L'hote emmene tout le monde avec lui : sa partie EST la partie. On
// previent avant de fondre, pour que les quatre fondus tournent ensemble et
// que personne ne voie l'ecran se figer.
if (HasAuthority())
{
KickRemoteClientsToMainMenu();
}
BeginQuitToMainMenu();
}
void AFpsPlayerController::HandleSessionTravelPending()
{
USessionSubsystem* Sessions = GetGameInstance() ? GetGameInstance()->GetSubsystem<USessionSubsystem>() : nullptr;
if (!Sessions || bQuittingToMainMenu)
{
return;
}
// Les ecrans ouverts resteraient affiches par-dessus le noir, curseur
// compris, et arriveraient tels quels dans la partie de l'ami.
if (bInventoryOpen)
{
SetInventoryScreenOpen(false, /*bShowCraftingTab=*/false);
}
if (bPauseMenuOpen)
{
ResumeGame();
}
ScreenFade->FadeOut(Sessions->GetTravelFadeDuration());
}
void AFpsPlayerController::Client_ReturnToMainMenu_Implementation()
{
UE_LOG(LogTemp, Log, TEXT("AFpsPlayerController : l'hote a quitte la partie, retour au menu."));
BeginQuitToMainMenu();
}
void AFpsPlayerController::KickRemoteClientsToMainMenu()
{
UWorld* World = GetWorld();
if (!World || !HasAuthority())
{
return;
}
for (FConstPlayerControllerIterator It = World->GetPlayerControllerIterator(); It; ++It)
{
AFpsPlayerController* Other = Cast<AFpsPlayerController>(It->Get());
// Soi-meme exclu : on a notre propre chemin, et l'hote n'est de toute
// facon pas joignable par une RPC Client_ depuis lui-meme.
if (Other && Other != this)
{
Other->Client_ReturnToMainMenu();
}
}
}
void AFpsPlayerController::BeginQuitToMainMenu()
{
// Un client peut avoir clique Quitter juste avant de recevoir l'ordre de
// l'hote : deux fondus simultanes se marcheraient dessus.
if (bQuittingToMainMenu)
{
return;
}
bQuittingToMainMenu = true;
// Le GameInstance doit savoir que ce depart est VOULU : sans ca, la
// connexion qui tombe pendant le fondu declencherait son filet de secours,
// qui chargerait le menu immediatement et couperait le fondu en deux.
if (USurvivalGameInstance* SurvivalGameInstance = GetGameInstance<USurvivalGameInstance>())
{
SurvivalGameInstance->BeginReturnToMainMenu();
}
if (QuitToMenuDialog)
{
QuitToMenuDialog->Close();
@@ -1103,10 +1310,13 @@ void AFpsPlayerController::HandleQuitToMenuConfirmed()
PauseMenu->SetVisibility(ESlateVisibility::Collapsed);
}
// On reste en pause pendant le fondu : relancer le monde une seconde pour
// l'abandonner ensuite laisserait le temps a la faim ou a une chute de tuer
// un joueur qui a deja quitte. C'est UScreenFadeComponent qui continue de
// ticker en pause, pas le monde.
// L'ecran d'inventaire ou de coffre resterait affiche par-dessus le noir,
// curseur compris. Sa fermeture rend le coffre au passage.
if (bInventoryOpen)
{
SetInventoryScreenOpen(false, /*bShowCraftingTab=*/false);
}
ScreenFade->FadeOut(QuitToMenuFadeDuration);
}
@@ -1132,6 +1342,17 @@ void AFpsPlayerController::HandleFadeFinished()
return;
}
// Fermer la session AVANT de voyager. Chez l'hote c'est ce qui la retire de
// la liste de tes amis a l'instant ou tu quittes, plutot qu'a l'arrivee sur
// le menu ; chez un client c'est ce qui libere sa place.
//
// Les autres joueurs, eux, perdront la connexion : c'est
// USurvivalGameInstance::HandleNetworkFailure qui les ramene a leur menu.
if (USessionSubsystem* Sessions = GetGameInstance() ? GetGameInstance()->GetSubsystem<USessionSubsystem>() : nullptr)
{
Sessions->LeaveSession();
}
UGameplayStatics::OpenLevelBySoftObjectPtr(this, MainMenuLevel);
}