(Feat) Fix Fade Loading
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -55,6 +55,15 @@ void AMainMenuPlayerController::BeginPlay()
|
||||
UE_LOG(LogTemp, Warning, TEXT("AMainMenuPlayerController : aucune AMenuCameraSpot active dans la map, le menu s'affichera sur la vue par defaut."));
|
||||
}
|
||||
|
||||
// APRES SetViewTarget, jamais avant : les images de chauffe doivent etre
|
||||
// rendues depuis l'angle qu'on va reellement montrer, sinon elles raffinent
|
||||
// un decor qu'on ne verra pas. Le voile est deja noir, donc rien ne fuit.
|
||||
USurvivalGameInstance* SurvivalGameInstance = GetGameInstance<USurvivalGameInstance>();
|
||||
if (SurvivalGameInstance)
|
||||
{
|
||||
SurvivalGameInstance->ResumeWorldRendering();
|
||||
}
|
||||
|
||||
if (MainMenuClass)
|
||||
{
|
||||
MainMenu = CreateWidget<UMainMenuWidget>(this, MainMenuClass);
|
||||
@@ -78,12 +87,31 @@ void AMainMenuPlayerController::BeginPlay()
|
||||
SetShowMouseCursor(true);
|
||||
|
||||
ScreenFade->OnFadeFinished.AddDynamic(this, &AMainMenuPlayerController::HandleFadeFinished);
|
||||
|
||||
// Meme raison qu'en jeu : on laisse le decor rendre quelques images derriere
|
||||
// le voile noir avant de l'eclaircir, le temps que Nanite et les textures
|
||||
// virtuelles se raffinent. Sans ca on voit le terrain passer de sa version
|
||||
// grossiere a la vraie, et les modeles apparaitre un a un.
|
||||
if (SurvivalGameInstance)
|
||||
{
|
||||
SurvivalGameInstance->BeginSceneWarmup(
|
||||
FSimpleDelegate::CreateUObject(this, &AMainMenuPlayerController::StartOpeningFade));
|
||||
}
|
||||
else
|
||||
{
|
||||
StartOpeningFade();
|
||||
}
|
||||
}
|
||||
|
||||
void AMainMenuPlayerController::StartOpeningFade()
|
||||
{
|
||||
ScreenFade->FadeIn(OpeningFadeDuration);
|
||||
|
||||
if (USessionSubsystem* Sessions = GetGameInstance() ? GetGameInstance()->GetSubsystem<USessionSubsystem>() : nullptr)
|
||||
{
|
||||
Sessions->OnHostFailed.AddDynamic(this, &AMainMenuPlayerController::HandleHostFailed);
|
||||
Sessions->OnJoinFailed.AddDynamic(this, &AMainMenuPlayerController::HandleJoinFailed);
|
||||
Sessions->OnTravelPending.AddDynamic(this, &AMainMenuPlayerController::HandleTravelPending);
|
||||
|
||||
// Revenir au menu depuis une partie laisse la session ouverte : elle
|
||||
// resterait annoncee a tes amis alors que plus personne n'est dedans.
|
||||
@@ -91,6 +119,14 @@ void AMainMenuPlayerController::BeginPlay()
|
||||
|
||||
UE_LOG(LogTemp, Log, TEXT("AMainMenuPlayerController : service en ligne [%s]."), *Sessions->GetOnlineServiceName());
|
||||
}
|
||||
|
||||
// On est arrive : le filet de secours reseau redevient actif pour la
|
||||
// prochaine partie. Sans cette remise a zero, un hote qui relance apres
|
||||
// avoir quitte ne serait plus jamais ramene au menu en cas de coupure.
|
||||
if (USurvivalGameInstance* SurvivalGameInstance = GetGameInstance<USurvivalGameInstance>())
|
||||
{
|
||||
SurvivalGameInstance->ClearReturnToMainMenu();
|
||||
}
|
||||
}
|
||||
|
||||
AMenuCameraSpot* AMainMenuPlayerController::PickCameraSpot()
|
||||
@@ -185,6 +221,31 @@ void AMainMenuPlayerController::HandleHostFailed(FText Reason)
|
||||
UGameplayStatics::OpenLevelBySoftObjectPtr(this, GameLevel, /*bAbsolute=*/true, TEXT("listen"));
|
||||
}
|
||||
|
||||
void AMainMenuPlayerController::HandleTravelPending()
|
||||
{
|
||||
USessionSubsystem* Sessions = GetGameInstance() ? GetGameInstance()->GetSubsystem<USessionSubsystem>() : nullptr;
|
||||
if (!Sessions)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Le menu reste lisible pendant le fondu mais cesse de repondre : cliquer
|
||||
// Jouer alors qu'on part deja rejoindre un ami serait un piege.
|
||||
if (MainMenu)
|
||||
{
|
||||
MainMenu->SetVisibility(ESlateVisibility::HitTestInvisible);
|
||||
}
|
||||
|
||||
// Exactement la duree que le sous-systeme attend : fondre plus lentement
|
||||
// laisserait voyager avant que l'ecran soit noir, plus vite laisserait un
|
||||
// ecran noir immobile.
|
||||
ScreenFade->FadeOut(Sessions->GetTravelFadeDuration());
|
||||
|
||||
// PendingAction reste a None VOLONTAIREMENT : ce n'est pas nous qui
|
||||
// chargerons la map, c'est le sous-systeme. HandleFadeFinished doit donc ne
|
||||
// rien faire quand ce fondu-la se termine.
|
||||
}
|
||||
|
||||
void AMainMenuPlayerController::HandleJoinFailed(FText Reason)
|
||||
{
|
||||
UE_LOG(LogTemp, Error, TEXT("AMainMenuPlayerController : connexion impossible -- %s"), *Reason.ToString());
|
||||
|
||||
@@ -59,6 +59,9 @@ void UScreenFadeComponent::EnsureWidget()
|
||||
|
||||
// Un widget n'existe que sur la machine du joueur local : inutile et
|
||||
// incorrect d'en creer un sur un serveur dedie.
|
||||
// ATTENTION : chez un client, ce test est FAUX au BeginPlay du controller --
|
||||
// son ULocalPlayer ne lui est attache qu'ensuite, dans SetPlayer(). Un
|
||||
// SnapToBlack() fait trop tot noircit donc un voile inexistant, en silence.
|
||||
if (!Controller->IsLocalController())
|
||||
{
|
||||
return;
|
||||
|
||||
@@ -380,6 +380,37 @@ void USessionSubsystem::HandleJoinSessionComplete(FName SessionName, EOnJoinSess
|
||||
return;
|
||||
}
|
||||
|
||||
// On ne voyage PAS tout de suite : on annonce, on laisse le controller
|
||||
// courant fondre au noir, puis on part. Sans ce delai, accepter une
|
||||
// invitation depuis le menu coupe l'image d'un coup -- le seul endroit du
|
||||
// jeu ou une transition n'etait pas fondue.
|
||||
PendingTravelURL = ConnectString;
|
||||
OnTravelPending.Broadcast();
|
||||
|
||||
UGameInstance* OwningGameInstance = GetGameInstance();
|
||||
if (TravelFadeDuration <= 0.f || !OwningGameInstance)
|
||||
{
|
||||
CommitPendingTravel();
|
||||
return;
|
||||
}
|
||||
|
||||
// Le TimerManager du GameInstance et non celui du monde : le monde courant
|
||||
// est justement celui qu'on s'apprete a quitter.
|
||||
OwningGameInstance->GetTimerManager().SetTimer(
|
||||
TravelTimer, this, &USessionSubsystem::CommitPendingTravel,
|
||||
TravelFadeDuration, /*bLoop=*/false);
|
||||
}
|
||||
|
||||
void USessionSubsystem::CommitPendingTravel()
|
||||
{
|
||||
if (PendingTravelURL.IsEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const FString TravelURL = MoveTemp(PendingTravelURL);
|
||||
PendingTravelURL.Reset();
|
||||
|
||||
APlayerController* Controller = GetGameInstance() ? GetGameInstance()->GetFirstLocalPlayerController() : nullptr;
|
||||
if (!Controller)
|
||||
{
|
||||
@@ -387,8 +418,8 @@ void USessionSubsystem::HandleJoinSessionComplete(FName SessionName, EOnJoinSess
|
||||
return;
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Log, TEXT("USessionSubsystem : voyage vers [%s]."), *ConnectString);
|
||||
Controller->ClientTravel(ConnectString, TRAVEL_Absolute);
|
||||
UE_LOG(LogTemp, Log, TEXT("USessionSubsystem : voyage vers [%s]."), *TravelURL);
|
||||
Controller->ClientTravel(TravelURL, TRAVEL_Absolute);
|
||||
}
|
||||
|
||||
void USessionSubsystem::HandleInviteAccepted(const bool bWasSuccessful, const int32 ControllerId, FUniqueNetIdPtr UserId, const FOnlineSessionSearchResult& InviteResult)
|
||||
|
||||
@@ -3,9 +3,15 @@
|
||||
|
||||
#include "SurvivalGameInstance.h"
|
||||
|
||||
#include "ContentStreaming.h"
|
||||
#include "EnhancedInputSubsystems.h"
|
||||
#include "Engine/Engine.h"
|
||||
#include "Engine/GameViewportClient.h"
|
||||
#include "Engine/LocalPlayer.h"
|
||||
#include "GameMapsSettings.h"
|
||||
#include "InputMappingContext.h"
|
||||
#include "Kismet/GameplayStatics.h"
|
||||
#include "SessionSubsystem.h"
|
||||
#include "SurvivalUserSettings.h"
|
||||
#include "UserSettings/EnhancedInputUserSettings.h"
|
||||
|
||||
@@ -32,6 +38,175 @@ void USurvivalGameInstance::Init()
|
||||
{
|
||||
UE_LOG(LogTemp, Error, TEXT("USurvivalGameInstance : GameUserSettingsClassName n'est pas configure dans DefaultEngine.ini, aucun reglage ne sera pris en compte."));
|
||||
}
|
||||
|
||||
// Filet de securite reseau. On s'abonne ici, une fois pour toutes : ces
|
||||
// evenements arrivent au pire moment, typiquement quand le monde est en
|
||||
// train d'etre detruit, et un abonnement porte par un acteur mourrait
|
||||
// justement avec lui.
|
||||
if (GEngine)
|
||||
{
|
||||
NetworkFailureHandle = GEngine->OnNetworkFailure().AddUObject(this, &USurvivalGameInstance::HandleNetworkFailure);
|
||||
TravelFailureHandle = GEngine->OnTravelFailure().AddUObject(this, &USurvivalGameInstance::HandleTravelFailure);
|
||||
}
|
||||
|
||||
PreLoadMapHandle = FCoreUObjectDelegates::PreLoadMap.AddUObject(this, &USurvivalGameInstance::HandlePreLoadMap);
|
||||
}
|
||||
|
||||
void USurvivalGameInstance::HandlePreLoadMap(const FString& MapName)
|
||||
{
|
||||
// On coupe AVANT que quoi que ce soit du nouveau monde n'existe. C'est le
|
||||
// premier controller pret qui rallumera, une fois son voile noir en place --
|
||||
// et il naitra forcement apres nous, ce qui garantit le bon ordre.
|
||||
SetWorldRenderingSuspended(true);
|
||||
UE_LOG(LogTemp, Log, TEXT("USurvivalGameInstance : chargement de [%s], rendu du monde suspendu."), *MapName);
|
||||
|
||||
// Le timer du GameInstance et non celui du monde : celui-ci est justement en
|
||||
// train d'etre detruit.
|
||||
GetTimerManager().SetTimer(
|
||||
RenderingResumeTimeout, this, &USurvivalGameInstance::ResumeWorldRendering,
|
||||
20.f, /*bLoop=*/false);
|
||||
}
|
||||
|
||||
void USurvivalGameInstance::BeginSceneWarmup(FSimpleDelegate OnComplete)
|
||||
{
|
||||
SceneWarmupCompleted = OnComplete;
|
||||
RemainingWarmupFrames = FMath::Max(SceneWarmupFrames, 0);
|
||||
WarmupStartTime = FPlatformTime::Seconds();
|
||||
|
||||
if (RemainingWarmupFrames <= 0)
|
||||
{
|
||||
SceneWarmupCompleted.ExecuteIfBound();
|
||||
SceneWarmupCompleted.Unbind();
|
||||
return;
|
||||
}
|
||||
|
||||
TickSceneWarmup();
|
||||
}
|
||||
|
||||
void USurvivalGameInstance::TickSceneWarmup()
|
||||
{
|
||||
const double Elapsed = FPlatformTime::Seconds() - WarmupStartTime;
|
||||
const bool bCapReached = Elapsed >= SceneWarmupMaxSeconds;
|
||||
|
||||
if (RemainingWarmupFrames > 0 && !bCapReached)
|
||||
{
|
||||
--RemainingWarmupFrames;
|
||||
|
||||
// SetTimerForNextTick et non un timer en secondes : une image rendue,
|
||||
// un tick. C'est l'unite dans laquelle Nanite et Lumen progressent.
|
||||
GetTimerManager().SetTimerForNextTick(this, &USurvivalGameInstance::TickSceneWarmup);
|
||||
return;
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Log, TEXT("USurvivalGameInstance : chauffe terminee -- %d images rendues en %.0f ms%s."),
|
||||
SceneWarmupFrames - RemainingWarmupFrames, Elapsed * 1000.0,
|
||||
bCapReached ? TEXT(" (plafond atteint)") : TEXT(""));
|
||||
|
||||
// Copie avant execution : le rappel declenche un fondu, qui pourrait
|
||||
// theoriquement relancer une chauffe et ecraser le delegue en pleine main.
|
||||
FSimpleDelegate Completed = SceneWarmupCompleted;
|
||||
SceneWarmupCompleted.Unbind();
|
||||
Completed.ExecuteIfBound();
|
||||
}
|
||||
|
||||
void USurvivalGameInstance::SetWorldRenderingSuspended(bool bSuspended)
|
||||
{
|
||||
if (UGameViewportClient* Viewport = GetGameViewportClient())
|
||||
{
|
||||
Viewport->bDisableWorldRendering = bSuspended;
|
||||
}
|
||||
}
|
||||
|
||||
void USurvivalGameInstance::ResumeWorldRendering()
|
||||
{
|
||||
GetTimerManager().ClearTimer(RenderingResumeTimeout);
|
||||
|
||||
// Streaming vide AVANT de rallumer. UpdateResourceStreaming(0, true) traite
|
||||
// tout sans avoir besoin d'un point de vue -- ce qui tombe bien, le rendu
|
||||
// est encore coupe donc personne ne lui en a fourni -- puis on bloque le
|
||||
// temps que les requetes aboutissent.
|
||||
//
|
||||
// Ce blocage a lieu ECRAN NOIR : c'est du temps de chargement, pas une
|
||||
// saccade. C'est exactement ce que le moteur fait lui-meme a l'ouverture
|
||||
// d'une map, et c'est ce qui evite de voir le terrain se raffiner et les
|
||||
// modeles apparaitre un a un.
|
||||
const double StartTime = FPlatformTime::Seconds();
|
||||
const int32 PendingRequests = IStreamingManager::Get().StreamAllResources(FMath::Max(StreamingFlushTimeLimit, 0.f));
|
||||
const double ElapsedMs = (FPlatformTime::Seconds() - StartTime) * 1000.0;
|
||||
|
||||
SetWorldRenderingSuspended(false);
|
||||
|
||||
UE_LOG(LogTemp, Log, TEXT("USurvivalGameInstance : rendu du monde repris apres %.0f ms de streaming (%d requete(s) restante(s))."),
|
||||
ElapsedMs, PendingRequests);
|
||||
}
|
||||
|
||||
void USurvivalGameInstance::Shutdown()
|
||||
{
|
||||
if (GEngine)
|
||||
{
|
||||
GEngine->OnNetworkFailure().Remove(NetworkFailureHandle);
|
||||
GEngine->OnTravelFailure().Remove(TravelFailureHandle);
|
||||
}
|
||||
|
||||
FCoreUObjectDelegates::PreLoadMap.Remove(PreLoadMapHandle);
|
||||
|
||||
Super::Shutdown();
|
||||
}
|
||||
|
||||
void USurvivalGameInstance::HandleNetworkFailure(UWorld* World, UNetDriver* NetDriver, ENetworkFailure::Type FailureType, const FString& ErrorString)
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("USurvivalGameInstance : echec reseau [%s] -- %s"),
|
||||
ENetworkFailure::ToString(FailureType), *ErrorString);
|
||||
|
||||
// Depart deja en cours : c'est le cas NORMAL quand l'hote quitte, puisqu'il
|
||||
// a prevenu tout le monde avant de couper. On laisse les fondus finir.
|
||||
if (bReturningToMainMenu)
|
||||
{
|
||||
UE_LOG(LogTemp, Log, TEXT("USurvivalGameInstance : depart deja en cours, echec ignore."));
|
||||
return;
|
||||
}
|
||||
|
||||
// Ici en revanche personne n'a prevenu : hote qui plante, cable arrache,
|
||||
// relais Steam qui lache. C'est le filet de secours, sans fondu -- mieux
|
||||
// vaut un retour brutal au menu qu'un monde mort.
|
||||
ReturnToMainMenu();
|
||||
}
|
||||
|
||||
void USurvivalGameInstance::HandleTravelFailure(UWorld* World, ETravelFailure::Type FailureType, const FString& ErrorString)
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("USurvivalGameInstance : echec de voyage [%s] -- %s"),
|
||||
ETravelFailure::ToString(FailureType), *ErrorString);
|
||||
|
||||
if (bReturningToMainMenu)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ReturnToMainMenu();
|
||||
}
|
||||
|
||||
void USurvivalGameInstance::ReturnToMainMenu()
|
||||
{
|
||||
bReturningToMainMenu = true;
|
||||
|
||||
// La session est morte avec la connexion : la detruire ici evite qu'elle
|
||||
// reste annoncee a tes amis, et surtout qu'elle bloque la creation de la
|
||||
// suivante.
|
||||
if (USessionSubsystem* Sessions = GetSubsystem<USessionSubsystem>())
|
||||
{
|
||||
Sessions->LeaveSession();
|
||||
}
|
||||
|
||||
// GetGameDefaultMap et pas un chemin en dur : c'est deja MenuScene dans
|
||||
// DefaultEngine.ini, et une seconde copie du chemin finirait par diverger.
|
||||
const FString MenuMap = UGameMapsSettings::GetGameDefaultMap();
|
||||
if (MenuMap.IsEmpty())
|
||||
{
|
||||
UE_LOG(LogTemp, Error, TEXT("USurvivalGameInstance : aucune GameDefaultMap, impossible de revenir au menu."));
|
||||
return;
|
||||
}
|
||||
|
||||
UGameplayStatics::OpenLevel(this, FName(*MenuMap), /*bAbsolute=*/true);
|
||||
}
|
||||
|
||||
void USurvivalGameInstance::RegisterMappableContexts()
|
||||
|
||||
@@ -121,7 +121,21 @@ public:
|
||||
|
||||
protected:
|
||||
virtual void BeginPlay() override;
|
||||
|
||||
/** Serveur uniquement. Chez un client, c'est AcknowledgePossession qui arrive. */
|
||||
virtual void OnPossess(APawn* InPawn) override;
|
||||
|
||||
/**
|
||||
* Le pendant CLIENT de OnPossess.
|
||||
*
|
||||
* OnPossess n'existe QUE sur le serveur : chez un client le pawn arrive par
|
||||
* replication et cette fonction n'est jamais appelee. Comme c'est elle qui
|
||||
* construisait le HUD, un joueur qui se connectait n'avait aucune interface --
|
||||
* ni viseur, ni barre rapide, ni jauges. Le piege est d'autant plus fourbe
|
||||
* que l'hote, lui, est bien le serveur et voit son HUD parfaitement.
|
||||
*/
|
||||
virtual void AcknowledgePossession(APawn* P) override;
|
||||
|
||||
virtual void SetupInputComponent() override;
|
||||
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
|
||||
|
||||
@@ -141,6 +155,16 @@ protected:
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "UI|Fondu")
|
||||
bool bFadeInOnStart = true;
|
||||
|
||||
/**
|
||||
* Délai maximal d'attente du pawn avant d'éclaircir quand même.
|
||||
*
|
||||
* Filet de sécurité pur : si la possession n'arrivait jamais, l'écran
|
||||
* resterait noir pour toujours et le joueur croirait le jeu planté. Mieux
|
||||
* vaut une vue bancale qu'un écran mort.
|
||||
*/
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "UI|Fondu", meta = (ClampMin = "1", Units = "s"))
|
||||
float StartupFadeTimeout = 10.f;
|
||||
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "UI|Fondu", meta = (ClampMin = "0.1"))
|
||||
float StartupFadeDuration = 1.5f;
|
||||
|
||||
@@ -307,6 +331,45 @@ protected:
|
||||
private:
|
||||
void CreateHudWidgets();
|
||||
|
||||
/**
|
||||
* Tout ce qu'il faut faire quand ce controller prend un pawn, quel que soit
|
||||
* le cote du reseau : creer le HUD s'il manque, rebrancher les widgets sur
|
||||
* le nouveau corps, ecouter sa mort.
|
||||
*
|
||||
* Appelee par OnPossess (serveur) ET AcknowledgePossession (client), donc
|
||||
* potentiellement deux fois chez l'hote qui est les deux a la fois. Elle
|
||||
* doit rester idempotente -- d'ou AddUniqueDynamic sur OnDied, et
|
||||
* CreateHudWidgets qui ne cree que ce qui n'existe pas encore.
|
||||
*/
|
||||
void SetupForPossessedPawn(APawn* InPawn);
|
||||
|
||||
/**
|
||||
* Éclaircit l'écran d'arrivée, une seule fois, et seulement quand le joueur
|
||||
* a réellement un corps.
|
||||
*
|
||||
* La condition n'est PAS « ai-je un pawn » mais « la caméra est-elle sur mon
|
||||
* pawn ». Chez un client, le contrôleur commence sur un `SpectatorPawn` né
|
||||
* au centre du monde -- sous le terrain -- et y reste encore un instant
|
||||
* APRÈS `AcknowledgePossession`. Mesuré sur une vraie connexion : au moment
|
||||
* où la possession est acquittée, `GetViewTarget()` vaut toujours
|
||||
* `SpectatorPawn_0`. Éclaircir là montre le dessous de la carte.
|
||||
*
|
||||
* Tant que la caméra n'y est pas, on repasse toutes les `StartupFadeRetry`
|
||||
* secondes. L'écran reste noir pendant ce temps -- le rendu du monde est
|
||||
* suspendu par le GameInstance depuis le chargement de la map.
|
||||
*/
|
||||
void TryStartupFadeIn();
|
||||
|
||||
/**
|
||||
* Éclaircit pour de bon, sans vérifier la caméra.
|
||||
* Appelée par le filet de sécurité : mieux vaut une vue bancale qu'un écran
|
||||
* noir définitif, qui se lit comme un plantage.
|
||||
*/
|
||||
void CompleteStartupFadeIn();
|
||||
|
||||
/** L'éclaircissement lui-même, après le temps de chauffe du monde. */
|
||||
void StartStartupFade();
|
||||
|
||||
/**
|
||||
* Echap. Une seule touche pour trois usages, d'ou une priorite explicite :
|
||||
* boite de confirmation, puis inventaire, puis pause. Sans cet ordre on se
|
||||
@@ -320,6 +383,37 @@ private:
|
||||
UFUNCTION()
|
||||
void HandleQuitToMenuConfirmed();
|
||||
|
||||
/**
|
||||
* L'hote s'en va : ce client doit partir aussi.
|
||||
*
|
||||
* Une RPC explicite et non un simple abandon de connexion. Quand l'hote
|
||||
* ferme sa partie, le moteur ne previent personne : les clients restent
|
||||
* dans un monde mort, avec le pawn fantome de l'hote encore plante devant
|
||||
* eux, jusqu'a un timeout qui peut ne jamais venir. On leur dit donc de
|
||||
* partir, et ils partent avec leur propre fondu.
|
||||
*/
|
||||
UFUNCTION(Client, Reliable)
|
||||
void Client_ReturnToMainMenu();
|
||||
|
||||
/**
|
||||
* Lance le depart : ferme les ecrans, previent le GameInstance, fond au noir.
|
||||
*
|
||||
* Idempotente -- un client peut recevoir l'ordre de l'hote alors qu'il avait
|
||||
* deja clique Quitter de son cote, et deux fondus simultanes se
|
||||
* marcheraient dessus.
|
||||
*/
|
||||
void BeginQuitToMainMenu();
|
||||
|
||||
/**
|
||||
* Envoie l'ordre de depart a tous les AUTRES joueurs. Serveur uniquement.
|
||||
*
|
||||
* Appele AVANT le fondu de l'hote et non apres : les clients ont ainsi
|
||||
* toute la duree de ce fondu pour recevoir le message et lancer le leur.
|
||||
* Prevenir a la fin donnerait un client qui decouvre la coupure sans
|
||||
* explication, ecran de jeu fige.
|
||||
*/
|
||||
void KickRemoteClientsToMainMenu();
|
||||
|
||||
/** Non : on referme la boite et on rend la main au menu de pause. */
|
||||
UFUNCTION()
|
||||
void HandleQuitToMenuCancelled();
|
||||
@@ -328,6 +422,17 @@ private:
|
||||
UFUNCTION()
|
||||
void HandleFadeFinished();
|
||||
|
||||
/**
|
||||
* Le joueur a accepte une invitation EN PLEINE PARTIE : on fond au noir
|
||||
* avant que le voyage vers l'autre hote parte.
|
||||
*
|
||||
* bQuittingToMainMenu reste faux : ce n'est pas nous qui chargeons la map
|
||||
* suivante, c'est USessionSubsystem. HandleFadeFinished doit donc laisser
|
||||
* passer ce fondu-la sans rien faire.
|
||||
*/
|
||||
UFUNCTION()
|
||||
void HandleSessionTravelPending();
|
||||
|
||||
/** L'ecran de reglages demande a se fermer : on rend la main au menu de pause. */
|
||||
UFUNCTION()
|
||||
void HandleSettingsCloseRequested();
|
||||
@@ -469,6 +574,18 @@ private:
|
||||
|
||||
FTimerHandle RespawnTimerHandle;
|
||||
|
||||
/** Le fondu d'arrivée attend le pawn. */
|
||||
bool bStartupFadePending = false;
|
||||
|
||||
FTimerHandle StartupFadeTimeoutHandle;
|
||||
|
||||
/** Réessai tant que la caméra n'est pas sur le pawn. */
|
||||
FTimerHandle StartupFadeRetryHandle;
|
||||
|
||||
|
||||
/** Période de réessai, en secondes. Assez court pour ne pas se voir. */
|
||||
static constexpr float StartupFadeRetry = 0.05f;
|
||||
|
||||
/** Surveillance de l'eloignement, actif seulement pendant qu'un coffre est ouvert. */
|
||||
FTimerHandle StorageDistanceTimerHandle;
|
||||
|
||||
|
||||
@@ -127,6 +127,9 @@ private:
|
||||
/** Tire un angle de vue au hasard, en evitant celui du dernier passage. */
|
||||
AMenuCameraSpot* PickCameraSpot();
|
||||
|
||||
/** L'eclaircissement lui-meme, apres la chauffe du decor. */
|
||||
void StartOpeningFade();
|
||||
|
||||
/** Le fondu vient de se terminer : on execute l'action mise de cote. */
|
||||
UFUNCTION()
|
||||
void HandleFadeFinished();
|
||||
@@ -162,6 +165,14 @@ private:
|
||||
UFUNCTION()
|
||||
void HandleJoinFailed(FText Reason);
|
||||
|
||||
/**
|
||||
* Une invitation vient d'aboutir : on fond au noir avant que le voyage
|
||||
* parte. C'est le seul chemin d'entree en partie qui ne passait pas par le
|
||||
* bouton Jouer, et donc le seul qui coupait l'image d'un coup.
|
||||
*/
|
||||
UFUNCTION()
|
||||
void HandleTravelPending();
|
||||
|
||||
/** Ferme le jeu sans rien demander. */
|
||||
void CloseGame();
|
||||
|
||||
|
||||
@@ -20,6 +20,16 @@ DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnSessionError, FText, Reason);
|
||||
/** Fin d'une recherche. SessionCount vaut 0 si personne n'heberge. */
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnSessionSearchFinished, bool, bWasSuccessful, int32, SessionCount);
|
||||
|
||||
/**
|
||||
* La connexion a abouti, le voyage part dans un instant.
|
||||
*
|
||||
* Existe uniquement pour laisser le temps d'un fondu : le sous-systeme sait ou
|
||||
* aller, mais le voile appartient au PlayerController -- et une invitation peut
|
||||
* etre acceptee depuis le menu comme en pleine partie, donc depuis deux
|
||||
* controllers differents. Chacun s'abonne et fond a sa maniere.
|
||||
*/
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnSessionTravelPending);
|
||||
|
||||
/**
|
||||
* Hebergement et connexion : creer une partie, la trouver, la rejoindre.
|
||||
*
|
||||
@@ -135,6 +145,22 @@ public:
|
||||
UPROPERTY(BlueprintAssignable, Category = "Session")
|
||||
FOnSessionSearchFinished OnSearchFinished;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "Session")
|
||||
FOnSessionTravelPending OnTravelPending;
|
||||
|
||||
/**
|
||||
* Duree laissee au fondu avant que le voyage parte.
|
||||
*
|
||||
* C'est LE sous-systeme qui la porte et non chaque controller : les deux
|
||||
* doivent fondre exactement le temps qu'il attend, sinon l'ecran voyage
|
||||
* avant d'etre noir. Une seule valeur, lue par tous.
|
||||
*
|
||||
* Pas de UPROPERTY editable : un GameInstanceSubsystem n'a aucun asset ou
|
||||
* la regler. Si elle doit bouger un jour, ce sera ici.
|
||||
*/
|
||||
UFUNCTION(BlueprintPure, Category = "Session")
|
||||
float GetTravelFadeDuration() const { return TravelFadeDuration; }
|
||||
|
||||
private:
|
||||
void HandleCreateSessionComplete(FName SessionName, bool bWasSuccessful);
|
||||
void HandleFindSessionsComplete(bool bWasSuccessful);
|
||||
@@ -173,6 +199,16 @@ private:
|
||||
/** Coupe proprement un delegue encore branche. Idempotent. */
|
||||
void ClearPendingDelegates();
|
||||
|
||||
/** Le fondu a eu son temps : on voyage pour de bon. */
|
||||
void CommitPendingTravel();
|
||||
|
||||
/** Adresse resolue de l'hote, en attente du fondu. */
|
||||
FString PendingTravelURL;
|
||||
|
||||
FTimerHandle TravelTimer;
|
||||
|
||||
float TravelFadeDuration = 1.f;
|
||||
|
||||
IOnlineSessionPtr SessionInterface;
|
||||
|
||||
TSharedPtr<FOnlineSessionSearch> SearchSettings;
|
||||
|
||||
@@ -28,6 +28,7 @@ class SURVIVAL_PROJET_API USurvivalGameInstance : public UGameInstance
|
||||
|
||||
public:
|
||||
virtual void Init() override;
|
||||
virtual void Shutdown() override;
|
||||
|
||||
/** Nom de la derniere camera de menu utilisee. NAME_None au premier lancement. */
|
||||
UFUNCTION(BlueprintPure, Category = "Menu")
|
||||
@@ -50,7 +51,87 @@ public:
|
||||
UFUNCTION(BlueprintCallable, Category = "Input")
|
||||
void RegisterMappableContexts();
|
||||
|
||||
/**
|
||||
* Un depart VOULU vers le menu commence : le filet de secours reseau doit
|
||||
* se taire.
|
||||
*
|
||||
* Sans ce drapeau, la connexion qui tombe pendant le fondu de sortie
|
||||
* declencherait HandleNetworkFailure, qui chargerait le menu sur-le-champ
|
||||
* et couperait le fondu en plein milieu -- exactement le saut d'image qu'on
|
||||
* cherchait a eviter en fondant.
|
||||
*/
|
||||
void BeginReturnToMainMenu() { bReturningToMainMenu = true; }
|
||||
|
||||
/** On est arrive au menu : le filet redevient actif pour la partie suivante. */
|
||||
void ClearReturnToMainMenu() { bReturningToMainMenu = false; }
|
||||
|
||||
/**
|
||||
* Rend la main au rendu du monde, coupe depuis le chargement de la map.
|
||||
*
|
||||
* A n'appeler QUE lorsque le voile de fondu est deja noir : le monde
|
||||
* reapparait alors derriere lui, invisible.
|
||||
*
|
||||
* Pourquoi couper le rendu plutot que poser un widget. Mesure sur une vraie
|
||||
* connexion : 2,4 secondes s'ecoulent entre `LoadMap` chez le client et la
|
||||
* naissance de son PlayerController, que le serveur lui envoie. Pendant ce
|
||||
* temps la map est chargee et le moteur la rend depuis une vue par defaut --
|
||||
* sous le terrain. Il n'existe alors AUCUN acteur a qui accrocher un voile,
|
||||
* et le composant de fondu vit justement sur ce controller absent. Le
|
||||
* viewport, lui, survit aux changements de map et n'appartient a personne.
|
||||
*
|
||||
* Attend aussi la fin du streaming avant de rendre la main : c'est le seul
|
||||
* instant du chargement ou l'on sait que rien ne sera montre tant qu'on n'a
|
||||
* pas fini. Sans ca, le joueur voit le terrain passer de sa version
|
||||
* grossiere a la vraie, et les modeles apparaitre un a un.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Chargement")
|
||||
void ResumeWorldRendering();
|
||||
|
||||
/**
|
||||
* Laisse le monde rendre SceneWarmupFrames images derriere le voile noir,
|
||||
* puis rappelle OnComplete.
|
||||
*
|
||||
* Compte des IMAGES et non des secondes, et c'est tout l'interet. Ce qui
|
||||
* reste a se raffiner apres le flush de streaming -- Nanite, les ombres
|
||||
* virtuelles, la convergence de Lumen -- progresse a chaque image RENDUE,
|
||||
* pas avec le temps qui passe. Une duree fixe donnerait quarante images sur
|
||||
* une machine rapide et cinq sur une machine lente : exactement celle qui en
|
||||
* aurait eu le plus besoin.
|
||||
*/
|
||||
void BeginSceneWarmup(FSimpleDelegate OnComplete);
|
||||
|
||||
protected:
|
||||
/**
|
||||
* Temps maximal accordé au streaming avant de montrer le monde, en secondes.
|
||||
*
|
||||
* Ce blocage se produit ECRAN NOIR, donc il ne se voit pas -- c'est du temps
|
||||
* de chargement, pas une saccade. Le dépasser vaut mieux que de montrer un
|
||||
* terrain qui se raffine sous les yeux du joueur, mais on le borne quand
|
||||
* même : une machine lente ne doit pas rester bloquée indéfiniment.
|
||||
*
|
||||
* Réglable sur BP_SurvivalGameInstance.
|
||||
*/
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Chargement", meta = (ClampMin = "0", Units = "s"))
|
||||
float StreamingFlushTimeLimit = 5.f;
|
||||
|
||||
/**
|
||||
* Nombre d'images rendues derriere le voile noir avant de l'eclaircir.
|
||||
* Voir BeginSceneWarmup(). Reglable sur BP_SurvivalGameInstance.
|
||||
*/
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Chargement", meta = (ClampMin = "0"))
|
||||
int32 SceneWarmupFrames = 45;
|
||||
|
||||
/**
|
||||
* Plafond de la chauffe, en secondes.
|
||||
*
|
||||
* Le compte d'images reste la mesure PRINCIPALE -- c'est la bonne unite.
|
||||
* Ceci n'est qu'un garde-fou : sur une machine tres lente, 45 images
|
||||
* pourraient prendre plusieurs secondes d'ecran noir, et mieux vaut un
|
||||
* decor pas tout a fait converge qu'une attente qui inquiete.
|
||||
*/
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Chargement", meta = (ClampMin = "0.1", Units = "s"))
|
||||
float SceneWarmupMaxSeconds = 2.f;
|
||||
|
||||
/**
|
||||
* Les mapping contexts dont les touches sont remappables. Mets-y IMC_Default.
|
||||
*
|
||||
@@ -63,6 +144,70 @@ protected:
|
||||
TArray<TObjectPtr<UInputMappingContext>> MappableContexts;
|
||||
|
||||
private:
|
||||
/**
|
||||
* L'hote a quitte, ou la connexion est tombee.
|
||||
*
|
||||
* Sans ce filet, un client dont l'hote ferme le jeu reste planté dans un
|
||||
* monde qui ne repond plus : le moteur coupe la connexion, mais rien ne le
|
||||
* ramene nulle part. C'est ici et pas sur le PlayerController parce que
|
||||
* celui-ci est justement detruit par l'echec -- le GameInstance est le seul
|
||||
* objet dont on soit certain qu'il vive encore a cet instant.
|
||||
*/
|
||||
void HandleNetworkFailure(UWorld* World, UNetDriver* NetDriver, ENetworkFailure::Type FailureType, const FString& ErrorString);
|
||||
|
||||
/** Meme filet pour un voyage qui echoue en route. */
|
||||
void HandleTravelFailure(UWorld* World, ETravelFailure::Type FailureType, const FString& ErrorString);
|
||||
|
||||
/**
|
||||
* Ferme la session en cours et rouvre le menu principal.
|
||||
*
|
||||
* La map du menu n'est PAS une propriete de plus : c'est deja la
|
||||
* GameDefaultMap du projet. En dupliquer le chemin ici donnerait deux
|
||||
* valeurs a tenir en phase, et celle-ci ne serait jamais testee.
|
||||
*/
|
||||
void ReturnToMainMenu();
|
||||
|
||||
/**
|
||||
* Une map va etre chargee : on coupe le rendu jusqu'a ce qu'un controller
|
||||
* soit pret.
|
||||
*
|
||||
* PreLoadMap et surtout PAS PostLoadMapWithWorld : ce dernier est diffuse
|
||||
* APRES le BeginPlay des acteurs. Sur une map chargee localement -- le menu,
|
||||
* ou la partie chez l'hote -- le controller rendait donc la main au rendu,
|
||||
* et on la lui reprenait juste apres. Ecran noir definitif, jusqu'au filet
|
||||
* de securite. Chez un client ca passait par accident, son controller
|
||||
* arrivant plus tard.
|
||||
*/
|
||||
void HandlePreLoadMap(const FString& MapName);
|
||||
|
||||
void SetWorldRenderingSuspended(bool bSuspended);
|
||||
|
||||
/** Une image de chauffe vient de passer : on decompte, ou on rappelle. */
|
||||
void TickSceneWarmup();
|
||||
|
||||
/** Images restantes a rendre avant d'eclaircir. */
|
||||
int32 RemainingWarmupFrames = 0;
|
||||
|
||||
/** Debut de la chauffe, pour appliquer le plafond en secondes. */
|
||||
double WarmupStartTime = 0.0;
|
||||
|
||||
/** Ce qu'on fait une fois la chauffe finie -- eclaircir, cote appelant. */
|
||||
FSimpleDelegate SceneWarmupCompleted;
|
||||
|
||||
FDelegateHandle NetworkFailureHandle;
|
||||
FDelegateHandle TravelFailureHandle;
|
||||
FDelegateHandle PreLoadMapHandle;
|
||||
|
||||
/**
|
||||
* Filet : si aucun controller ne rend la main -- connexion qui n'aboutit
|
||||
* pas, spawn rate -- on rallume quand meme. Un ecran noir definitif se lit
|
||||
* comme un plantage, et c'est pire que le defaut qu'on corrige.
|
||||
*/
|
||||
FTimerHandle RenderingResumeTimeout;
|
||||
|
||||
/** Un retour au menu est deja en cours, voulu ou non. */
|
||||
bool bReturningToMainMenu = false;
|
||||
|
||||
/**
|
||||
* Stocke un NOM et pas un index : les index changent des qu'on ajoute ou
|
||||
* supprime une camera dans la map, et on retomberait sur autre chose.
|
||||
|
||||
@@ -17,7 +17,10 @@ public class Survival_projet : ModuleRules
|
||||
// l'asset, pas decider quand il s'allume.
|
||||
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "EnhancedInput", "GameplayTags", "UMG", "Niagara" });
|
||||
|
||||
PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" });
|
||||
// EngineSettings : UGameMapsSettings::GetGameDefaultMap(), qui evite de
|
||||
// redeclarer le chemin de la map de menu dans le C++ alors qu'il est
|
||||
// deja dans DefaultEngine.ini.
|
||||
PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore", "EngineSettings" });
|
||||
|
||||
// Coop Steam : on ne depend QUE de l'API generique (OnlineSubsystem) et de
|
||||
// ses helpers (OnlineSubsystemUtils, qui porte le resolveur d'adresses).
|
||||
|
||||
Reference in New Issue
Block a user