// Fill out your copyright notice in the Description page of Project Settings. #pragma once #include "CoreMinimal.h" #include "Engine/GameInstance.h" #include "SurvivalGameInstance.generated.h" class UInputMappingContext; class ULocalPlayer; /** * Le seul objet qui survit aux changements de map. * * C'est l'equivalent le plus proche d'un singleton DontDestroyOnLoad d'Unity : * il naît au lancement du jeu et meurt a sa fermeture. Tout le reste -- * GameMode, GameState, PlayerController, pawns, widgets -- est detruit et * recree a chaque OpenLevel. * * Y mettre : les reglages, la sauvegarde en cours, et plus tard l'hebergement * et le rejoindre-une-partie. Surtout pas d'etat de gameplay, qui appartient au * monde et doit disparaitre avec lui. */ UCLASS() class SURVIVAL_PROJET_API USurvivalGameInstance : public UGameInstance { GENERATED_BODY() 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") FName GetLastMenuCameraName() const { return LastMenuCameraName; } UFUNCTION(BlueprintCallable, Category = "Menu") void SetLastMenuCameraName(FName SpotName) { LastMenuCameraName = SpotName; } /** * Declare les IMC remappables aupres des reglages d'input du joueur. * * Appelee A LA DEMANDE, par l'ecran de reglages avant de lire les touches, et * non depuis un evenement de demarrage. Raison : UserSettings n'est cree que * dans UEnhancedInputLocalPlayerSubsystem::PlayerControllerChanged, et * ULocalPlayer diffuse ses evenements AVANT de prevenir ses sous-systemes -- * tout hook precoce arrive donc trop tot, quel qu'il soit. * * Idempotente : le moteur ignore un contexte deja connu. */ UFUNCTION(BlueprintCallable, Category = "Input") void RegisterMappableContexts(); /** * Traduit les touches par defaut vers la disposition clavier du joueur. * * Les IMC sont ecrits en QWERTY US -- EKeys::W pour avancer -- et Windows livre * au moteur une touche DEJA traduite par la disposition : en AZERTY, la touche * du haut du bloc de deplacement arrive comme EKeys::Z et le mapping "W" ne se * declenche jamais. On repose donc chaque touche a la position equivalente. * Le detail du mecanisme est dans KeyboardLayout.h. * * Ne touche QUE les mappings que le joueur n'a pas remappes lui-meme : un * choix explicite l'emporte toujours sur une deduction. Et sur un clavier * QWERTY la conversion rend la touche d'origine, donc rien n'est ecrit du tout * et le profil reste vierge. * * Declenchee UNE FOIS par RegisterMappableContexts(), au premier enregistrement * des contextes ; ensuite seul "Reinitialiser" la redemande. Elle appartient a * l'initialisation, pas au rafraichissement d'ecran -- l'onglet Touches se * rafraichit apres chaque remappage, et repasser dessus reprendrait sa touche * au joueur qui aurait justement choisi celle de l'IMC (la lettre W sur un * AZERTY, par exemple : identique au defaut, donc indiscernable d'un mapping * jamais touche). */ UFUNCTION(BlueprintCallable, Category = "Input") void ApplyKeyboardLayoutToDefaultKeys(); /** * 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. * * Ici et PAS sur le pawn : le menu principal n'a aucun pawn, et l'ecran de * reglages doit pourtant afficher les touches. C'est le critere habituel -- * "est-ce que ca doit survivre au changement de map ?" -- qui envoie cette * liste sur le GameInstance. */ UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Input") TArray> 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; /** * La disposition clavier a deja ete appliquee dans cette session. * * Pas de UPROPERTY(config) : ce n'est PAS "l'a-t-on deja fait un jour". Les * surcharges posees, elles, sont persistees par Enhanced Input, et elles * suffisent a rendre l'operation sans effet au lancement suivant. Ce drapeau * ne protege que du re-declenchement en cours de session. */ bool bKeyboardLayoutApplied = 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. * * Ne sert qu'a eviter de retirer deux fois de suite le meme angle quand le * joueur revient au menu -- d'ou le Transient, ca n'a pas a etre sauvegarde. */ UPROPERTY(Transient) FName LastMenuCameraName = NAME_None; };