// Fill out your copyright notice in the Description page of Project Settings. #include "MainMenuPlayerController.h" #include "CharacterAppearanceComponent.h" #include "CharacterCustomizationWidget.h" #include "CharacterPreviewActor.h" #include "ConfirmDialogWidget.h" #include "EngineUtils.h" #include "Kismet/GameplayStatics.h" #include "Kismet/KismetSystemLibrary.h" #include "MainMenuWidget.h" #include "MenuCameraSpot.h" #include "ScreenFadeComponent.h" #include "SessionSubsystem.h" #include "SettingsMenuWidget.h" #include "SurvivalGameInstance.h" #include "SurvivalUserSettings.h" #define LOCTEXT_NAMESPACE "MainMenu" namespace { // Le menu reste sous le voile de fondu, qui est en 1000. constexpr int32 ZOrderMainMenu = 0; // L'ecran de reglages recouvre le menu, la boite de confirmation passe // devant les deux, et le fondu reste au-dessus de tout a 1000. constexpr int32 ZOrderMainMenuSettings = 50; constexpr int32 ZOrderQuitDialog = 100; } AMainMenuPlayerController::AMainMenuPlayerController() { // Meme composant que dans le jeu : un seul fondu a maintenir. ScreenFade = CreateDefaultSubobject(TEXT("ScreenFade")); bShowMouseCursor = true; QuitDialogMessage = LOCTEXT("QuitConfirm", "Quitter le jeu ?"); } void AMainMenuPlayerController::BeginPlay() { Super::BeginPlay(); // L'ecran part du noir : sans ca on verrait une frame de la map, cadree // n'importe comment, avant que le fondu ne commence. ScreenFade->SnapToBlack(); if (AMenuCameraSpot* Spot = PickCameraSpot()) { // Sans blend : on arrive directement sur l'angle tire au sort. SetViewTarget(Spot); } else { 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(); if (SurvivalGameInstance) { SurvivalGameInstance->ResumeWorldRendering(); } if (MainMenuClass) { MainMenu = CreateWidget(this, MainMenuClass); if (MainMenu) { MainMenu->AddToViewport(ZOrderMainMenu); } } else { UE_LOG(LogTemp, Warning, TEXT("AMainMenuPlayerController : MainMenuClass n'est pas assignee, aucun menu ne s'affichera.")); } // GameAndUI et non UIOnly : UIOnly avale aussi la console et les raccourcis // du moteur, ce qui rend le debug penible. Il n'y a de toute facon aucun // input de jeu dans cette map, rien ne peut passer a travers. FInputModeGameAndUI Mode; Mode.SetLockMouseToViewportBehavior(EMouseLockMode::DoNotLock); Mode.SetHideCursorDuringCapture(false); SetInputMode(Mode); 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() : 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. Sessions->LeaveSession(); 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()) { SurvivalGameInstance->ClearReturnToMainMenu(); } } AMenuCameraSpot* AMainMenuPlayerController::PickCameraSpot() { UWorld* World = GetWorld(); if (!World) { return nullptr; } USurvivalGameInstance* SurvivalGameInstance = GetGameInstance(); const FName LastUsed = SurvivalGameInstance ? SurvivalGameInstance->GetLastMenuCameraName() : NAME_None; // TActorIterator plutot que GetAllActorsOfClass : pas de tableau d'AActor* // intermediaire, pas de cast par element. TArray AllSpots; TArray Candidates; for (TActorIterator It(World); It; ++It) { AMenuCameraSpot* Spot = *It; if (!Spot->bEnabled) { continue; } AllSpots.Add(Spot); // On ecarte l'angle du dernier passage pour ne pas revoir deux fois de // suite le meme decor en revenant au menu. if (Spot->GetFName() != LastUsed) { Candidates.Add(Spot); } } // Une seule camera dans la map : on la reprend plutot que de n'afficher // aucune vue. if (Candidates.Num() == 0) { Candidates = MoveTemp(AllSpots); } if (Candidates.Num() == 0) { return nullptr; } AMenuCameraSpot* Chosen = Candidates[FMath::RandRange(0, Candidates.Num() - 1)]; if (SurvivalGameInstance) { SurvivalGameInstance->SetLastMenuCameraName(Chosen->GetFName()); } return Chosen; } void AMainMenuPlayerController::StartGame() { // Le fondu est deja lance : on ignore le second clic plutot que d'empiler // deux chargements. if (PendingAction != EMenuPendingAction::None) { return; } // L'ecran de customisation s'intercale ICI, entre le clic et le fondu : // c'est le choix fait pour ce projet, on ne lance pas une partie sans etre // passe par son personnage. Sans classe assignee, on retombe sur l'ancien // comportement -- pratique pour tester le reste sans le traverser. if (CustomizationMenuClass && !CustomizationMenu) { PendingAction = EMenuPendingAction::OpenCustomization; if (MainMenu) { MainMenu->SetVisibility(ESlateVisibility::HitTestInvisible); } // On fond au noir AVANT de bouger la camera : le mannequin est ailleurs // dans la meme map, et un travelling jusqu'a lui traverserait le decor. ScreenFade->FadeOut(CustomizationFadeDuration); return; } if (GameLevel.IsNull()) { UE_LOG(LogTemp, Error, TEXT("AMainMenuPlayerController : GameLevel n'est pas assignee, impossible de lancer la partie.")); return; } PendingAction = EMenuPendingAction::StartGame; // Le menu reste visible pendant le fondu mais cesse de repondre : cliquer // Quitter pendant le fondu vers la partie serait un piege. // // Le test de visibilite est indispensable : quand on arrive ici depuis // l'ecran de customisation, le menu a ete Collapsed a son ouverture, et // HitTestInvisible le RESSUSCITERAIT -- on verrait reapparaitre Jouer, // Reglages et Quitter par-dessus le personnage qu'on vient de valider. // HitTestInvisible rend non cliquable, il ne cache pas. if (MainMenu && MainMenu->GetVisibility() != ESlateVisibility::Collapsed) { MainMenu->SetVisibility(ESlateVisibility::HitTestInvisible); } ScreenFade->FadeOut(LeaveFadeDuration); } // ---------------------------------------------------------------------- // Ecran de customisation // ---------------------------------------------------------------------- void AMainMenuPlayerController::OpenCustomization() { // Cherche au moment de l'ouverture et pas au BeginPlay : c'est le seul // endroit ou l'on est sur que la map est entierement chargee, et un // mannequin absent doit rester rattrapable. if (!CharacterPreview) { for (TActorIterator It(GetWorld()); It; ++It) { CharacterPreview = *It; break; } } if (!CharacterPreview) { UE_LOG(LogTemp, Warning, TEXT("AMainMenuPlayerController : aucun ACharacterPreviewActor dans la map, l'ecran de customisation est saute.")); // On ne bloque pas le joueur pour un acteur manquant : meme logique // qu'un echec de session, il a clique Jouer, il veut jouer. CustomizationMenuClass = nullptr; StartGame(); return; } CustomizationMenu = CreateWidget(this, CustomizationMenuClass); if (!CustomizationMenu) { return; } // TOUJOURS l'apparence par defaut, jamais celle persistee : cet ecran est // une etape de CREATION, pas une reprise. Le jour ou un lobby verifiera que // les quatre joueurs ont valide leur personnage, il lui faudra un point de // depart identique pour tout le monde. // // La persistance ne devient pas inutile pour autant : elle sert au respawn, // et surtout a qui arrive par invitation Steam -- celui-la ne passe jamais // par ici et doit bien avoir un visage. // // « Par defaut » veut dire le DefaultAppearance du mannequin, pas les zeros // du C++ : le personnage de depart se regle ainsi dans BP_CharacterPreview, // a l'oeil et sans recompiler. C'est le meme reglage que celui pose au // BeginPlay, donc l'ouverture de l'ecran ne change rien a ce qu'on voyait. const UCharacterAppearanceComponent* PreviewAppearance = CharacterPreview->GetAppearanceComponent(); CustomizationMenu->SetupCustomization( CharacterPreview, PreviewAppearance ? PreviewAppearance->DefaultAppearance : FCharacterAppearance()); CustomizationMenu->OnConfirmed.AddDynamic(this, &AMainMenuPlayerController::HandleCustomizationConfirmed); CustomizationMenu->OnCancelled.AddDynamic(this, &AMainMenuPlayerController::HandleCustomizationCancelled); // Sans blend : l'ecran est noir a cet instant, personne ne voit la bascule. // C'est justement ce que le fondu achete -- un travelling depuis l'angle de // menu jusqu'au mannequin traverserait le decor de la map. SetViewTarget(CharacterPreview); if (MainMenu) { MainMenu->SetVisibility(ESlateVisibility::Collapsed); } CustomizationMenu->AddToViewport(); // L'ecran a des boutons ET un clic-glisser : il lui faut la souris, et le // mode UI-only l'empeche de tomber dans le jeu. SetInputMode(FInputModeUIOnly().SetWidgetToFocus(CustomizationMenu->TakeWidget())); SetShowMouseCursor(true); ScreenFade->FadeIn(CustomizationFadeDuration); } void AMainMenuPlayerController::HandleCustomizationConfirmed() { // Persiste ET annonce -- mais nous sommes dans le menu, aucun serveur // n'ecoute encore. Seule la persistance compte ici ; l'annonce partira du // AnnounceCharacterAppearance() d'AFpsPlayerController, une fois la partie // chargee et le PlayerState en place. if (USurvivalUserSettings* Settings = USurvivalUserSettings::Get()) { Settings->SetCharacterAppearance(CustomizationMenu->GetAppearance()); Settings->SaveSettings(); } CloseCustomization(); // La classe est mise de cote le temps du lancement : sans ca, StartGame // rouvrirait l'ecran qu'on vient de fermer. CustomizationMenuClass = nullptr; StartGame(); } void AMainMenuPlayerController::HandleCustomizationCancelled() { // Le fondu de retour est deja lance : on ignore la demande plutot que de le // relancer depuis le debut. La garde compte plus depuis qu'Echap declenche // ce chemin -- une touche se repete bien plus facilement qu'un clic. if (PendingAction != EMenuPendingAction::None) { return; } // Meme mecanique qu'a l'aller : on fond, PUIS on bascule la vue. Le retour // se termine dans HandleFadeFinished. PendingAction = EMenuPendingAction::CloseCustomization; if (CustomizationMenu) { CustomizationMenu->SetVisibility(ESlateVisibility::HitTestInvisible); } ScreenFade->FadeOut(CustomizationFadeDuration); } void AMainMenuPlayerController::CloseCustomization() { if (CustomizationMenu) { CustomizationMenu->RemoveFromParent(); CustomizationMenu = nullptr; } // Le menu principal reprend la main : son propre mode d'input est pose par // le Blueprint, on remet simplement le curseur dans un etat coherent. SetInputMode(FInputModeUIOnly()); SetShowMouseCursor(true); } void AMainMenuPlayerController::HandleHostFailed(FText Reason) { UE_LOG(LogTemp, Warning, TEXT("AMainMenuPlayerController : session indisponible (%s), la partie se lance hors ligne."), *Reason.ToString()); // Sans session on ouvre quand meme en serveur d'ecoute plutot qu'en solo // sec : l'ecart de cout est nul a un joueur, et ca evite d'avoir deux // facons differentes de demarrer une partie a maintenir. UGameplayStatics::OpenLevelBySoftObjectPtr(this, GameLevel, /*bAbsolute=*/true, TEXT("listen")); } void AMainMenuPlayerController::HandleTravelPending() { USessionSubsystem* Sessions = GetGameInstance() ? GetGameInstance()->GetSubsystem() : 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()); // Ici pas de repli : le joueur voulait rejoindre un ami precis. On le rend // simplement a son menu, ecran remonte -- sans quoi un echec le laisserait // devant un noir definitif, ce qui se lit comme un plantage. PendingAction = EMenuPendingAction::None; if (MainMenu) { MainMenu->SetVisibility(ESlateVisibility::Visible); } ScreenFade->FadeIn(LeaveFadeDuration); } void AMainMenuPlayerController::QuitGame() { // Une partie est deja en cours de chargement : la question n'a plus de sens. if (PendingAction != EMenuPendingAction::None) { return; } // Sans boite assignee on ne bloque pas le joueur dans son menu : on ferme. if (!QuitDialogClass) { UE_LOG(LogTemp, Warning, TEXT("AMainMenuPlayerController : QuitDialogClass n'est pas assignee, fermeture sans confirmation.")); CloseGame(); return; } // Creee au premier clic seulement, puis reutilisee : une boite qu'on ouvre // et ferme plusieurs fois n'a aucune raison d'etre reconstruite. if (!QuitDialog) { QuitDialog = CreateWidget(this, QuitDialogClass); if (!QuitDialog) { CloseGame(); return; } QuitDialog->OnConfirmed.AddDynamic(this, &AMainMenuPlayerController::HandleQuitConfirmed); QuitDialog->OnCancelled.AddDynamic(this, &AMainMenuPlayerController::HandleQuitCancelled); QuitDialog->AddToViewport(ZOrderQuitDialog); } QuitDialog->Open(QuitDialogMessage); // Le menu reste lisible derriere mais cesse de repondre : sans ca, on // pourrait relancer une partie alors que la question est encore posee. if (MainMenu) { MainMenu->SetVisibility(ESlateVisibility::HitTestInvisible); } } void AMainMenuPlayerController::OpenSettings() { // Une partie se charge deja : ouvrir un ecran maintenant serait un piege. if (PendingAction != EMenuPendingAction::None) { return; } if (!SettingsMenuClass) { UE_LOG(LogTemp, Warning, TEXT("AMainMenuPlayerController : SettingsMenuClass n'est pas assignee, les reglages ne s'ouvriront pas.")); return; } // Creee a la premiere ouverture seulement, puis reutilisee. if (!SettingsMenu) { SettingsMenu = CreateWidget(this, SettingsMenuClass); if (!SettingsMenu) { return; } SettingsMenu->OnCloseRequested.AddDynamic(this, &AMainMenuPlayerController::HandleSettingsCloseRequested); SettingsMenu->AddToViewport(ZOrderMainMenuSettings); // Meme ecran qu'en jeu, mais notre pile est plus courte : sa boite se // pose a 60, devant les reglages et derriere celle du bouton Quitter. SettingsMenu->SetDialogZOrder(ZOrderMainMenuSettings + 10); } SettingsMenu->Open(); // Le menu reste lisible derriere mais cesse de repondre : sans ca on // pourrait lancer une partie a travers l'ecran de reglages. if (MainMenu) { MainMenu->SetVisibility(ESlateVisibility::HitTestInvisible); } } void AMainMenuPlayerController::HandleSettingsCloseRequested() { if (SettingsMenu) { SettingsMenu->Close(); } if (MainMenu) { MainMenu->SetVisibility(ESlateVisibility::Visible); } } void AMainMenuPlayerController::HandleQuitConfirmed() { // Pas de fondu : le joueur vient de confirmer, le faire patienter une // seconde de plus ne serait qu'agacant. CloseGame(); } void AMainMenuPlayerController::HandleQuitCancelled() { if (QuitDialog) { QuitDialog->Close(); } if (MainMenu) { MainMenu->SetVisibility(ESlateVisibility::Visible); } } void AMainMenuPlayerController::CloseGame() { UKismetSystemLibrary::QuitGame(this, this, EQuitPreference::Quit, /*bIgnorePlatformRestrictions=*/false); } void AMainMenuPlayerController::HandleFadeFinished() { // L'ecran est noir : c'est ICI qu'on bascule la vue sur le mannequin, et // pas au clic. PendingAction est remise a None tout de suite, sinon le // fondu d'eclaircissement qui suit repasserait par la meme branche. if (PendingAction == EMenuPendingAction::OpenCustomization) { PendingAction = EMenuPendingAction::None; OpenCustomization(); return; } if (PendingAction == EMenuPendingAction::CloseCustomization) { PendingAction = EMenuPendingAction::None; CloseCustomization(); if (MainMenu) { MainMenu->SetVisibility(ESlateVisibility::Visible); } // Un nouvel angle au retour plutot que celui d'avant : PickCameraSpot // evite deja le dernier utilise, donc le menu ne se rouvre jamais sur // exactement la meme image. if (AMenuCameraSpot* Spot = PickCameraSpot()) { SetViewTarget(Spot); } ScreenFade->FadeIn(CustomizationFadeDuration); return; } // Le delegue se declenche aussi a la fin du fondu d'ouverture, ou il n'y a // rien a faire : c'est PendingAction qui distingue les cas. if (PendingAction != EMenuPendingAction::StartGame) { return; } // C'est le sous-systeme qui ouvrira la map, et seulement une fois la session // reellement creee : ouvrir avant donnerait une partie que personne ne peut // rejoindre. Le cas d'echec passe par HandleHostFailed, qui charge quand meme. if (USessionSubsystem* Sessions = GetGameInstance() ? GetGameInstance()->GetSubsystem() : nullptr) { Sessions->HostSession(GameLevel, USessionSubsystem::DefaultMaxPlayers); } else { UE_LOG(LogTemp, Warning, TEXT("AMainMenuPlayerController : USessionSubsystem introuvable, partie lancee hors ligne.")); UGameplayStatics::OpenLevelBySoftObjectPtr(this, GameLevel, /*bAbsolute=*/true, TEXT("listen")); } } void AMainMenuPlayerController::EndPlay(const EEndPlayReason::Type EndPlayReason) { if (SettingsMenu) { SettingsMenu->RemoveFromParent(); SettingsMenu = nullptr; } if (QuitDialog) { QuitDialog->RemoveFromParent(); QuitDialog = nullptr; } if (MainMenu) { MainMenu->RemoveFromParent(); MainMenu = nullptr; } Super::EndPlay(EndPlayReason); } #undef LOCTEXT_NAMESPACE