(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
+111 -17
View File
@@ -129,6 +129,7 @@ qu'une compilation propre l'est vraiment.
| Réglages | `SurvivalUserSettings`, `SettingsMenuWidget`, `SettingsTabWidget`, `SettingsGameplayWidget`, `SettingRowWidget` |
| Fondus, boîtes de dialogue | `ScreenFadeComponent`, `ConfirmDialogWidget` |
| Coop Steam (sessions) | `SessionSubsystem`, `SurvivalGameMode`, `SurvivalGameState`, `SurvivalPlayerState` |
| Pseudo au-dessus des joueurs | `PlayerNameplateComponent`, `PlayerNameplateWidget` |
### Coop Steam — décisions figées
@@ -242,6 +243,99 @@ exactement le cas d'`APickupItem::OnRep_ItemData`.
ne fait de Tick : il suffit qu'`OnRep_Slots()` rediffuse ce délégué. C'est le dividende de la
règle « les widgets s'abonnent à des délégués », et il faut la tenir pour les systèmes à venir.
**Un client arrive en SPECTATEUR, et la caméra y reste un instant.** C'est la cause du « on voit
sous la carte » à la connexion, et elle est contre-intuitive : le pawn est bien créé **et bien
placé**, mais `GetViewTarget()` vaut encore `SpectatorPawn_0` — né au centre du monde, donc sous
le terrain. Mesuré sur une vraie connexion :
```
FadeIn démarre ViewTarget=SpectatorPawn_0 PawnLoc=(11319, -16530, 1805)
AcknowledgePossession Pawn=BP_FpsPlayer_C_0 ViewTarget=SpectatorPawn_0
```
La caméra bascule sur le corps **après** l'acquittement de la possession. La bonne condition pour
éclaircir n'est donc pas « ai-je un pawn » mais **`GetViewTarget() == GetPawn()`** — d'où la
boucle de réessai de `TryStartupFadeIn()`, avec `CompleteStartupFadeIn()` comme filet au bout de
`StartupFadeTimeout` (un écran noir définitif se lit comme un plantage).
**Et rien ne doit être rendu avant.** Entre le chargement de la map et ce moment-là, aucun acteur
n'existe à qui accrocher un voile — le composant de fondu vit justement sur le contrôleur absent.
`USurvivalGameInstance` coupe donc `bDisableWorldRendering` sur le viewport, seul objet qui survive
au changement de map sans appartenir à personne, et c'est le contrôleur qui le rend une fois son
voile noir en place.
**`PreLoadMap` et surtout PAS `PostLoadMapWithWorld`** pour cette coupure. Le second est diffusé
**après** le `BeginPlay` des acteurs : sur une map chargée localement — le menu, ou la partie chez
l'hôte — le contrôleur rendait la main au rendu, et on la lui reprenait juste après. Écran noir
définitif jusqu'au filet de sécurité. Chez un client ça passait *par accident*, son contrôleur
arrivant plus tard — donc invisible si l'on ne teste que ce cas-là.
*Note : `ReceivedPlayer()` arrive AVANT `BeginPlay()` chez un client, et `IsLocalController()` y est
déjà vrai — ce n'est donc pas là qu'est le problème, contrairement à ce qu'on pourrait croire.*
**`IsLocalController()` est indispensable sur tout ce qui touche à l'écran.** Sur la machine de
l'hôte, chaque joueur distant a AUSSI un `AFpsPlayerController` côté serveur. Sans cette garde, le
contrôleur d'un ami qui se connecte va poser un fondu et rallumer le rendu **du viewport de
l'hôte**, à un instant totalement arbitraire.
### Étiquettes de pseudo
- **Le serveur ne peut PAS deviner le pseudo Steam d'un client** : l'API d'identité ne connaît que
l'utilisateur local de chaque machine. C'est donc le client qui l'annonce
(`Server_SetPlayerNickname`), et le serveur l'écrit sur le `PlayerState`, déjà répliqué par le
moteur. Sans ça on lit « Player » au-dessus des quatre joueurs — c'est le nom par défaut que le
moteur met dans l'URL de voyage.
- **Espace ÉCRAN, pas Monde** : l'étiquette fait face à la caméra *par construction*, sans calcul
d'orientation, et n'est jamais occultée par le décor. Mais `SWorldWidgetScreenLayer::Tick`
n'applique **aucune échelle de distance** — le pseudo garderait une taille en pixels constante
pendant que le personnage rétrécit, ce que l'œil lit comme une étiquette qui *grossit* en
s'éloignant. D'où l'échelle calculée à la main en 1/d, et l'opacité en fondu sur la dernière
bande avant `MaxDrawDistance`.
- Le composant ne réplique **rien** : le pseudo vit déjà sur le `PlayerState`.
### Le pop-in au chargement — deux causes, deux remèdes
**Les requêtes** — textures et LOD de meshes. `IStreamingManager::StreamAllResources()` les traite
toutes (`UpdateResourceStreaming(0, true)` n'a pas besoin de point de vue, ce qui tombe bien : le
rendu est encore coupé) puis bloque jusqu'à ce qu'elles aboutissent. Appelé dans
`ResumeWorldRendering()`, donc **écran noir** : c'est du temps de chargement, pas une saccade.
Mesuré : ~80 ms, 0 requête restante.
**Ce qui converge par IMAGE** — Nanite, ombres virtuelles, Lumen. Aucun flush ne peut les forcer :
il leur faut des images rendues. D'où `BeginSceneWarmup()`, qui laisse le monde rendre derrière le
voile encore noir avant de l'éclaircir.
**Il compte des IMAGES, pas des secondes**, et c'est tout l'intérêt : une durée fixe donnerait
quarante images sur une machine rapide et cinq sur une machine lente — c'est-à-dire le moins de
chauffe à celle qui en a le plus besoin. `SetTimerForNextTick` = un tick, une image.
`SceneWarmupMaxSeconds` n'est qu'un **plafond**, pas la mesure ; le log dit « plafond atteint »
quand il s'applique, pour savoir lequel des deux réglages ajuster.
Ordre non négociable dans le menu : `SetViewTarget` **puis** `ResumeWorldRendering()`. L'inverse
ferait chauffer le décor vu depuis l'angle par défaut, c'est-à-dire pas celui qu'on va montrer.
**`OnPossess` n'existe QUE sur le serveur.** Chez un client, le pawn arrive par réplication et
`OnPossess` n'est jamais appelée — son pendant est **`AcknowledgePossession`**. Tout ce qui doit
se produire à la prise de contrôle (création du HUD, rebranchement des widgets, abonnement à
`OnDied`) passe donc par `SetupForPossessedPawn()`, appelée des deux côtés et **idempotente** :
chez l'hôte, qui est serveur *et* client local, les deux chemins l'exécutent. D'où
`AddUniqueDynamic` sur `OnDied`, sinon la mort déclencherait deux fondus et deux respawns.
Symptôme quand on l'oublie : un joueur qui rejoint n'a aucune interface, alors que l'hôte a la
sienne — donc invisible en solo comme en test chez soi.
**Une connexion qui tombe ne ramène personne nulle part.** Le moteur coupe le lien et s'arrête là.
`USurvivalGameInstance` s'abonne donc à `GEngine->OnNetworkFailure()` et `OnTravelFailure()` pour
détruire la session et rouvrir le menu. C'est sur le GameInstance et pas sur le PlayerController
parce que celui-ci est justement détruit par l'échec.
**Quand l'hôte quitte, tout le monde quitte — et on le dit explicitement.** Le moteur ne prévient
personne : sans RPC, les clients restent dans un monde mort, avec le **pawn fantôme de l'hôte**
encore planté devant eux, jusqu'à un timeout qui peut ne jamais venir. `HandleQuitToMenuConfirmed`
appelle donc `KickRemoteClientsToMainMenu()` **avant** de lancer son propre fondu, pour que les
quatre fondus tournent ensemble. Le filet `OnNetworkFailure` reste pour ce qui n'est pas annoncé —
hôte qui plante, câble arraché — et se tait pendant un départ voulu grâce à
`BeginReturnToMainMenu()`, sinon il chargerait le menu en plein milieu du fondu.
| Système | État |
|---|---|
| Déplacement, crouch | ✅ gratuit via `ACharacter` / `CharacterMovement` |
@@ -463,26 +557,26 @@ Le détail du système de craft — API, câblage éditeur, dépannage — est d
## État actuel et suite
Boucle complète : récolter → **fabriquer****cuire** → ranger → consommer → survivre
mourir → réapparaître. Autour : menu principal, menu de pause, et un écran de réglages dont **seul
l'onglet « Jeu & Contrôles » est rempli**.
Boucle complète **en coop jusqu'à 4 joueurs** : récolter → fabriquer → cuire → ranger
consommer → survivre → mourir → réapparaître. Autour : menu principal, menu de pause, écran de
réglages, étiquettes de pseudo Steam.
Le craft est en place de bout en bout : `DA_RecipeBook`, onglets Inventaire / Fabrication,
grille de recettes filtrée par poste accessible, panneau de détail avec compteurs colorés.
Restent à faire quand le besoin viendra : l'**acteur établi** qui appellera
`AddAvailableStation(Workbench)`, les **boutons de filtre** par poste (`SetStationFilter` existe
déjà et `ECraftingStation::Count` sert de sentinelle « tout »), et les vraies recettes — celles
en place sont deux `DA_Craft_Test_*` bâties sur `DA_Branch` et `DA_Rocher`.
Le portage réseau est terminé pour tout le gameplay existant — voir *Réplication* plus haut pour
le détail et les pièges. Les transitions de map sont propres de bout en bout : plus de flash, plus
de pop-in, départ groupé quand l'hôte quitte.
Suite immédiate sur les réglages, dans l'ordre : onglet **Affichage** (le moteur fait tout,
prévoir la confirmation à rebours de 10 s sur la résolution), onglet **Graphismes** (les dix
curseurs de `UGameUserSettings` ; végétation et distance d'affichage comptent double avec
SoStylized), onglet **Audio** quand des `SoundClass` existeront, et enfin le **remappage des
touches** via `UEnhancedInputUserSettings` — le seul morceau réellement coûteux.
Les vraies recettes restent à écrire : celles en place sont deux `DA_Craft_Test_*` bâties sur
`DA_Branch` et `DA_Rocher`. Les **boutons de filtre** par poste ne sont pas faits non plus
(`SetStationFilter` existe déjà, `ECraftingStation::Count` sert de sentinelle « tout »).
Prochaine étape gameplay recommandée : **spawner de ressources avec repousse**
(`AResourceSpawner`, liste pondérée de DataAssets, rayon, densité, délai). Sans lui la carte se
vide en dix minutes.
vide en dix minutes. À écrire **serveur-autoritaire d'emblée** — c'est un acteur du monde.
Ensuite, par ordre de valeur : objet visible en main, établi, coffres, sauvegarde,
cycle jour/nuit.
Ensuite, par ordre de valeur : objet visible en main, sauvegarde, cycle jour/nuit.
> **Systèmes présents dans `Source/` mais pas encore décrits ici** : `MusicDirector`,
> `MusicSubsystem`, `MusicPlaylistDataAsset`, `SettingsDisplayWidget`, `SettingsGraphicsWidget`,
> `SettingsControlsWidget`, `SettingKeyRowWidget`, `KeyCaptureWidget`. Ils sont arrivés en dehors
> des sessions qui ont produit ce document — à documenter quand on y touchera, plutôt que de
> décrire de mémoire un code qu'on n'a pas lu.
@@ -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`
* au centre du monde -- sous le terrain -- et y reste encore un instant
* APRÈS `AcknowledgePossession`. Mesuré sur une vraie connexion : au moment
* la possession est acquittée, `GetViewTarget()` vaut toujours
* `SpectatorPawn_0`. Éclaircir 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).