(Feat) Fix Fade Loading
This commit is contained in:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user