434 lines
13 KiB
C++
434 lines
13 KiB
C++
// Fill out your copyright notice in the Description page of Project Settings.
|
|
|
|
|
|
#include "MainMenuPlayerController.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"
|
|
|
|
#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<UScreenFadeComponent>(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<USurvivalGameInstance>();
|
|
if (SurvivalGameInstance)
|
|
{
|
|
SurvivalGameInstance->ResumeWorldRendering();
|
|
}
|
|
|
|
if (MainMenuClass)
|
|
{
|
|
MainMenu = CreateWidget<UMainMenuWidget>(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<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.
|
|
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<USurvivalGameInstance>())
|
|
{
|
|
SurvivalGameInstance->ClearReturnToMainMenu();
|
|
}
|
|
}
|
|
|
|
AMenuCameraSpot* AMainMenuPlayerController::PickCameraSpot()
|
|
{
|
|
UWorld* World = GetWorld();
|
|
if (!World)
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
USurvivalGameInstance* SurvivalGameInstance = GetGameInstance<USurvivalGameInstance>();
|
|
const FName LastUsed = SurvivalGameInstance ? SurvivalGameInstance->GetLastMenuCameraName() : NAME_None;
|
|
|
|
// TActorIterator plutot que GetAllActorsOfClass : pas de tableau d'AActor*
|
|
// intermediaire, pas de cast par element.
|
|
TArray<AMenuCameraSpot*> AllSpots;
|
|
TArray<AMenuCameraSpot*> Candidates;
|
|
for (TActorIterator<AMenuCameraSpot> 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;
|
|
}
|
|
|
|
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.
|
|
if (MainMenu)
|
|
{
|
|
MainMenu->SetVisibility(ESlateVisibility::HitTestInvisible);
|
|
}
|
|
|
|
ScreenFade->FadeOut(LeaveFadeDuration);
|
|
}
|
|
|
|
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<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());
|
|
|
|
// 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<UConfirmDialogWidget>(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<USettingsMenuWidget>(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()
|
|
{
|
|
// 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 deux 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<USessionSubsystem>() : 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
|