(Feat) Add MainMenu

This commit is contained in:
2026-07-28 14:10:17 +02:00
parent ed7c7fd991
commit 175bef228c
33 changed files with 2331 additions and 12 deletions
+99 -3
View File
@@ -17,6 +17,7 @@
#include "ItemDataAsset.h"
#include "PickupItem.h"
#include "SurvivalStatsComponent.h"
#include "SurvivalUserSettings.h"
// Sets default values
AFpsPlayer::AFpsPlayer()
@@ -72,6 +73,21 @@ void AFpsPlayer::BeginPlay()
SurvivalStats->OnDied.AddDynamic(this, &AFpsPlayer::HandleDeath);
// Les reglages d'abord : le pawn est recree a chaque respawn et repart des
// valeurs de son CDO, il doit donc les relire lui-meme. L'abonnement couvre
// ensuite les changements faits en cours de partie depuis le menu de pause.
ApplyUserSettings();
if (USurvivalUserSettings* Settings = USurvivalUserSettings::Get())
{
Settings->OnSurvivalSettingsApplied.AddDynamic(this, &AFpsPlayer::ApplyUserSettings);
}
// Le FOV est interpole chaque frame vers BaseFOV : sans ce calage initial on
// verrait la camera s'ouvrir lentement jusqu'a la valeur reglee a chaque
// apparition.
FirstPersonCamera->SetFieldOfView(BaseFOV);
// On enregistre le mapping context aupres du sous-systeme Enhanced Input
// du joueur local. Sans ca, aucune touche n'est lue.
if (const APlayerController* PlayerController = Cast<APlayerController>(GetController()))
@@ -91,6 +107,45 @@ void AFpsPlayer::BeginPlay()
}
}
void AFpsPlayer::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
// Le singleton de reglages survit au monde : sans ce desabonnement, chaque
// cadavre laisserait derriere lui un abonne mort de plus.
if (USurvivalUserSettings* Settings = USurvivalUserSettings::Get())
{
Settings->OnSurvivalSettingsApplied.RemoveDynamic(this, &AFpsPlayer::ApplyUserSettings);
}
Super::EndPlay(EndPlayReason);
}
void AFpsPlayer::ApplyUserSettings()
{
USurvivalUserSettings* Settings = USurvivalUserSettings::Get();
if (!Settings)
{
// Pas de reglages : on garde les valeurs du Blueprint. Le jeu reste
// jouable, il n'est simplement pas configurable.
UE_LOG(LogTemp, Warning, TEXT("AFpsPlayer : USurvivalUserSettings introuvable, les valeurs du Blueprint sont conservees."));
return;
}
LookSensitivityX = Settings->GetLookMultiplierX();
LookSensitivityY = Settings->GetLookMultiplierY();
bInvertLookY = Settings->GetInvertLookY();
BaseFOV = Settings->GetFieldOfView();
CameraShakeMultiplier = Settings->GetCameraShakeMultiplier();
bSprintToggleMode = Settings->GetSprintToggle();
bCrouchToggleMode = Settings->GetCrouchToggle();
// Repasser en maintien alors que la touche n'est plus enfoncee laisserait le
// personnage bloque en sprint : on repart d'un etat propre.
if (!bSprintToggleMode && bIsSprinting)
{
StopSprint();
}
}
// Called every frame
void AFpsPlayer::Tick(float DeltaTime)
{
@@ -193,9 +248,12 @@ void AFpsPlayer::UpdateCameraEffects(float DeltaTime)
void AFpsPlayer::AddCameraPunch(float Strength)
{
// Le reglage s'applique ICI et pas sur chaque source d'impact : tout ce qui
// secoue la camera passe par cette porte, y compris les explosions et les
// reculs d'arme a venir. A 0, la camera ne bouge plus du tout.
// Mis en attente plutot qu'applique directement : c'est UpdateCameraEffects
// qui l'injecte progressivement dans le ressort.
PendingPunch += Strength;
PendingPunch += Strength * CameraShakeMultiplier;
}
void AFpsPlayer::Landed(const FHitResult& Hit)
@@ -263,14 +321,17 @@ void AFpsPlayer::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent
if (SprintAction)
{
// Le mode maintien / bascule se decide dans les handlers et non ici :
// rebrancher les bindings a chaud demanderait de reconstruire l'input
// component a chaque changement de reglage.
Input->BindAction(SprintAction, ETriggerEvent::Started, this, &AFpsPlayer::StartSprint);
Input->BindAction(SprintAction, ETriggerEvent::Completed, this, &AFpsPlayer::StopSprint);
Input->BindAction(SprintAction, ETriggerEvent::Completed, this, &AFpsPlayer::HandleSprintReleased);
}
if (CrouchAction)
{
Input->BindAction(CrouchAction, ETriggerEvent::Started, this, &AFpsPlayer::StartCrouch);
Input->BindAction(CrouchAction, ETriggerEvent::Completed, this, &AFpsPlayer::StopCrouch);
Input->BindAction(CrouchAction, ETriggerEvent::Completed, this, &AFpsPlayer::HandleCrouchReleased);
}
if (InteractAction)
@@ -330,6 +391,13 @@ void AFpsPlayer::StartSprint()
return;
}
// En mode bascule, la meme touche coupe le sprint.
if (bSprintToggleMode && bIsSprinting)
{
StopSprint();
return;
}
bIsSprinting = true;
GetCharacterMovement()->MaxWalkSpeed = SprintSpeed;
}
@@ -340,6 +408,17 @@ void AFpsPlayer::StopSprint()
GetCharacterMovement()->MaxWalkSpeed = WalkSpeed;
}
void AFpsPlayer::HandleSprintReleased()
{
// En bascule, relacher la touche ne doit rien faire.
if (bSprintToggleMode)
{
return;
}
StopSprint();
}
void AFpsPlayer::UseSelectedItem()
{
const FInventorySlot Selected = InventoryComponent->GetSelectedSlot();
@@ -514,10 +593,27 @@ void AFpsPlayer::ClearTransientInputStates()
void AFpsPlayer::StartCrouch()
{
// En mode bascule, la meme touche releve le personnage.
if (bCrouchToggleMode && bIsCrouched)
{
StopCrouch();
return;
}
StopSprint();
Crouch();
}
void AFpsPlayer::HandleCrouchReleased()
{
if (bCrouchToggleMode)
{
return;
}
StopCrouch();
}
void AFpsPlayer::StopCrouch()
{
// UnCrouch() echoue silencieusement s'il y a un plafond : le perso
@@ -4,6 +4,7 @@
#include "FpsPlayerController.h"
#include "Blueprint/UserWidget.h"
#include "ConfirmDialogWidget.h"
#include "EnhancedInputComponent.h"
#include "FpsPlayer.h"
#include "GameFramework/GameModeBase.h"
@@ -14,9 +15,14 @@
#include "InventoryComponent.h"
#include "InteractionPromptWidget.h"
#include "InventoryWidget.h"
#include "Kismet/GameplayStatics.h"
#include "PauseMenuWidget.h"
#include "ScreenFadeComponent.h"
#include "SettingsMenuWidget.h"
#include "SurvivalStatsWidget.h"
#define LOCTEXT_NAMESPACE "PauseMenu"
namespace
{
// Ordre d'empilement des widgets plein ecran, du fond vers l'avant.
@@ -35,6 +41,12 @@ namespace
constexpr int32 ZOrderInteractionPrompt = 10;
constexpr int32 ZOrderInventory = 20;
constexpr int32 ZOrderHotbar = 30;
// Le menu de pause passe devant tout le HUD, et la boite de confirmation
// devant lui. Meme echelle que dans le menu principal.
constexpr int32 ZOrderPauseMenu = 100;
constexpr int32 ZOrderSettingsMenu = 150;
constexpr int32 ZOrderQuitToMenuDialog = 200;
}
AFpsPlayerController::AFpsPlayerController()
@@ -45,12 +57,29 @@ AFpsPlayerController::AFpsPlayerController()
// Rappel Unreal : ce constructeur tourne aussi dans l'editeur, sur le CDO.
// Il ne doit contenir que de la construction, jamais de logique de jeu.
ScreenFade = CreateDefaultSubobject<UScreenFadeComponent>(TEXT("ScreenFade"));
QuitToMenuMessage = LOCTEXT("QuitToMenuConfirm", "Retourner au menu principal ?\nTa progression sera perdue.");
}
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
// repondrait qu'au clic droit maintenu. On ne suppose donc jamais l'etat
// laisse par la map precedente, on le repose.
if (IsLocalController())
{
SetInputMode(FInputModeGameOnly());
SetShowMouseCursor(false);
}
// Le delegue se declenche a la fin de CHAQUE fondu : c'est
// bQuittingToMainMenu qui distingue le fondu de sortie du reste.
ScreenFade->OnFadeFinished.AddDynamic(this, &AFpsPlayerController::HandleFadeFinished);
if (bFadeInOnStart)
{
ScreenFade->FadeIn(StartupFadeDuration);
@@ -157,6 +186,15 @@ void AFpsPlayerController::SetupInputComponent()
{
Input->BindAction(HotbarScrollAction, ETriggerEvent::Triggered, this, &AFpsPlayerController::HandleHotbarScroll);
}
if (PauseAction)
{
Input->BindAction(PauseAction, ETriggerEvent::Started, this, &AFpsPlayerController::HandlePauseInput);
}
else
{
UE_LOG(LogTemp, Warning, TEXT("AFpsPlayerController : PauseAction n'est pas assignee, le menu de pause sera inaccessible."));
}
}
UInventoryComponent* AFpsPlayerController::GetPawnInventory() const
@@ -298,6 +336,24 @@ void AFpsPlayerController::CreateHudWidgets()
}
}
if (!PauseMenu)
{
if (PauseMenuClass)
{
PauseMenu = CreateWidget<UPauseMenuWidget>(this, PauseMenuClass);
if (PauseMenu)
{
PauseMenu->AddToViewport(ZOrderPauseMenu);
// Cree une fois pour toutes et simplement cache, comme
// l'inventaire : le menu de pause survit ainsi aux respawns.
PauseMenu->SetVisibility(ESlateVisibility::Collapsed);
}
}
else
{
UE_LOG(LogTemp, Warning, TEXT("AFpsPlayerController : PauseMenuClass n'est pas assignee, le menu de pause ne s'ouvrira pas."));
}
}
}
void AFpsPlayerController::ToggleInventory()
@@ -356,8 +412,277 @@ void AFpsPlayerController::ToggleInventory()
}
}
void AFpsPlayerController::HandlePauseInput()
{
// La boite a le focus et consomme deja Echap elle-meme. Si elle l'a perdu,
// on l'ignore quand meme : repondre ici refermerait le menu en laissant la
// question posee.
if (bQuitToMenuDialogOpen)
{
return;
}
// L'ecran de reglages consomme Echap lui-meme tant qu'il a le focus. Comme
// pour la boite, on ne repond ici que s'il l'a perdu.
if (bSettingsOpen)
{
HandleSettingsCloseRequested();
return;
}
// Echap sert d'abord a annuler ce qui est ouvert. Fermer l'inventaire est
// ce que le joueur attend en premier.
if (bInventoryOpen)
{
ToggleInventory();
return;
}
if (bPauseMenuOpen)
{
ResumeGame();
return;
}
OpenPauseMenu();
}
void AFpsPlayerController::OpenPauseMenu()
{
if (!PauseMenu || bPauseMenuOpen)
{
return;
}
// Mettre en pause pendant l'ecran de mort figerait le timer de respawn : le
// joueur resterait devant un ecran noir dont plus rien ne le sortirait.
if (GetWorldTimerManager().IsTimerActive(RespawnTimerHandle))
{
return;
}
bPauseMenuOpen = true;
PauseMenu->SetVisibility(ESlateVisibility::Visible);
// GameAndUI et non UIOnly, meme raison que l'inventaire : en UIOnly plus
// aucune action Enhanced Input ne passe, et Echap ne refermerait rien.
FInputModeGameAndUI Mode;
Mode.SetLockMouseToViewportBehavior(EMouseLockMode::DoNotLock);
Mode.SetHideCursorDuringCapture(false);
SetInputMode(Mode);
SetShowMouseCursor(true);
// Pas de DisableInput sur le pawn ici, contrairement a l'inventaire : le
// monde est reellement fige, et les actions du pawn n'ont pas
// "Trigger When Paused" -- elles ne se declenchent donc plus d'elles-memes.
SetPause(true);
}
void AFpsPlayerController::ResumeGame()
{
if (!bPauseMenuOpen)
{
return;
}
// Reprendre alors qu'une boite ou les reglages sont encore ouverts les
// laisserait affiches par-dessus la partie relancee.
if (bQuitToMenuDialogOpen)
{
HandleQuitToMenuCancelled();
}
if (bSettingsOpen)
{
HandleSettingsCloseRequested();
}
bPauseMenuOpen = false;
if (PauseMenu)
{
PauseMenu->SetVisibility(ESlateVisibility::Collapsed);
}
SetPause(false);
SetInputMode(FInputModeGameOnly());
SetShowMouseCursor(false);
}
void AFpsPlayerController::OpenSettings()
{
if (bSettingsOpen)
{
return;
}
if (!SettingsMenuClass)
{
UE_LOG(LogTemp, Warning, TEXT("AFpsPlayerController : SettingsMenuClass n'est pas assignee, les reglages ne s'ouvriront pas."));
return;
}
// Creee a la premiere ouverture seulement : un ecran de reglages qu'on ne
// visite peut-etre jamais n'a pas a peser sur chaque demarrage de partie.
if (!SettingsMenu)
{
SettingsMenu = CreateWidget<USettingsMenuWidget>(this, SettingsMenuClass);
if (!SettingsMenu)
{
return;
}
SettingsMenu->OnCloseRequested.AddDynamic(this, &AFpsPlayerController::HandleSettingsCloseRequested);
SettingsMenu->AddToViewport(ZOrderSettingsMenu);
}
bSettingsOpen = true;
SettingsMenu->Open();
// Le menu de pause reste lisible derriere mais cesse de repondre : sans ca
// on pourrait cliquer Reprendre a travers l'ecran de reglages.
if (PauseMenu)
{
PauseMenu->SetVisibility(ESlateVisibility::HitTestInvisible);
}
}
void AFpsPlayerController::HandleSettingsCloseRequested()
{
bSettingsOpen = false;
if (SettingsMenu)
{
SettingsMenu->Close();
}
if (PauseMenu)
{
PauseMenu->SetVisibility(ESlateVisibility::Visible);
}
}
void AFpsPlayerController::RequestQuitToMainMenu()
{
// Le fondu de sortie est deja lance : on ignore le second clic plutot que
// d'empiler deux chargements.
if (bQuittingToMainMenu)
{
return;
}
if (MainMenuLevel.IsNull())
{
UE_LOG(LogTemp, Error, TEXT("AFpsPlayerController : MainMenuLevel n'est pas assignee, impossible de retourner au menu."));
return;
}
// Sans boite assignee on ne bloque pas le joueur dans sa partie : on part.
if (!QuitToMenuDialogClass)
{
UE_LOG(LogTemp, Warning, TEXT("AFpsPlayerController : QuitToMenuDialogClass n'est pas assignee, retour au menu sans confirmation."));
HandleQuitToMenuConfirmed();
return;
}
// Creee au premier clic seulement, puis reutilisee.
if (!QuitToMenuDialog)
{
QuitToMenuDialog = CreateWidget<UConfirmDialogWidget>(this, QuitToMenuDialogClass);
if (!QuitToMenuDialog)
{
return;
}
QuitToMenuDialog->OnConfirmed.AddDynamic(this, &AFpsPlayerController::HandleQuitToMenuConfirmed);
QuitToMenuDialog->OnCancelled.AddDynamic(this, &AFpsPlayerController::HandleQuitToMenuCancelled);
QuitToMenuDialog->AddToViewport(ZOrderQuitToMenuDialog);
}
bQuitToMenuDialogOpen = true;
QuitToMenuDialog->Open(QuitToMenuMessage);
// Le menu reste lisible derriere mais cesse de repondre : sans ca, on
// pourrait cliquer Reprendre alors que la question est encore posee.
if (PauseMenu)
{
PauseMenu->SetVisibility(ESlateVisibility::HitTestInvisible);
}
}
void AFpsPlayerController::HandleQuitToMenuConfirmed()
{
bQuitToMenuDialogOpen = false;
bQuittingToMainMenu = true;
if (QuitToMenuDialog)
{
QuitToMenuDialog->Close();
}
if (PauseMenu)
{
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.
ScreenFade->FadeOut(QuitToMenuFadeDuration);
}
void AFpsPlayerController::HandleQuitToMenuCancelled()
{
bQuitToMenuDialogOpen = false;
if (QuitToMenuDialog)
{
QuitToMenuDialog->Close();
}
if (PauseMenu)
{
PauseMenu->SetVisibility(ESlateVisibility::Visible);
}
}
void AFpsPlayerController::HandleFadeFinished()
{
if (!bQuittingToMainMenu)
{
return;
}
// La pause vit sur les WorldSettings, que OpenLevel detruit avec le monde.
// On la leve quand meme : un monde qu'on quitte en etat de pause est un
// piege si le chargement echoue.
SetPause(false);
UGameplayStatics::OpenLevelBySoftObjectPtr(this, MainMenuLevel);
}
void AFpsPlayerController::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
if (SettingsMenu)
{
SettingsMenu->RemoveFromParent();
SettingsMenu = nullptr;
}
if (QuitToMenuDialog)
{
QuitToMenuDialog->RemoveFromParent();
QuitToMenuDialog = nullptr;
}
if (PauseMenu)
{
PauseMenu->RemoveFromParent();
PauseMenu = nullptr;
}
if (InventoryWidget)
{
InventoryWidget->RemoveFromParent();
@@ -392,3 +717,5 @@ void AFpsPlayerController::EndPlay(const EEndPlayReason::Type EndPlayReason)
Super::EndPlay(EndPlayReason);
}
#undef LOCTEXT_NAMESPACE
@@ -10,6 +10,7 @@
#include "MainMenuWidget.h"
#include "MenuCameraSpot.h"
#include "ScreenFadeComponent.h"
#include "SettingsMenuWidget.h"
#include "SurvivalGameInstance.h"
#define LOCTEXT_NAMESPACE "MainMenu"
@@ -19,7 +20,9 @@ namespace
// Le menu reste sous le voile de fondu, qui est en 1000.
constexpr int32 ZOrderMainMenu = 0;
// La boite de confirmation passe devant le menu, mais sous le fondu.
// 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 ZOrderSettingsMenu = 50;
constexpr int32 ZOrderQuitDialog = 100;
}
@@ -201,6 +204,56 @@ void AMainMenuPlayerController::QuitGame()
}
}
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(ZOrderSettingsMenu);
}
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
@@ -238,6 +291,12 @@ void AMainMenuPlayerController::HandleFadeFinished()
void AMainMenuPlayerController::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
if (SettingsMenu)
{
SettingsMenu->RemoveFromParent();
SettingsMenu = nullptr;
}
if (QuitDialog)
{
QuitDialog->RemoveFromParent();
@@ -20,6 +20,11 @@ void UMainMenuWidget::NativeConstruct()
QuitButton->OnClicked.AddDynamic(this, &UMainMenuWidget::HandleQuitClicked);
}
if (SettingsButton)
{
SettingsButton->OnClicked.AddDynamic(this, &UMainMenuWidget::HandleSettingsClicked);
}
// Les boutons de coop sont visibles mais inertes : ils annoncent la couleur
// sans promettre une partie qui echouerait.
if (HostButton)
@@ -48,3 +53,11 @@ void UMainMenuWidget::HandleQuitClicked()
Controller->QuitGame();
}
}
void UMainMenuWidget::HandleSettingsClicked()
{
if (AMainMenuPlayerController* Controller = GetOwningPlayer<AMainMenuPlayerController>())
{
Controller->OpenSettings();
}
}
@@ -0,0 +1,71 @@
// Fill out your copyright notice in the Description page of Project Settings.
#include "PauseMenuWidget.h"
#include "Components/Button.h"
#include "FpsPlayerController.h"
void UPauseMenuWidget::NativeConstruct()
{
Super::NativeConstruct();
if (ResumeButton)
{
ResumeButton->OnClicked.AddDynamic(this, &UPauseMenuWidget::HandleResumeClicked);
}
if (QuitToMenuButton)
{
QuitToMenuButton->OnClicked.AddDynamic(this, &UPauseMenuWidget::HandleQuitToMenuClicked);
}
if (SettingsButton)
{
SettingsButton->OnClicked.AddDynamic(this, &UPauseMenuWidget::HandleSettingsClicked);
}
}
void UPauseMenuWidget::NativeDestruct()
{
if (ResumeButton)
{
ResumeButton->OnClicked.RemoveDynamic(this, &UPauseMenuWidget::HandleResumeClicked);
}
if (SettingsButton)
{
SettingsButton->OnClicked.RemoveDynamic(this, &UPauseMenuWidget::HandleSettingsClicked);
}
if (QuitToMenuButton)
{
QuitToMenuButton->OnClicked.RemoveDynamic(this, &UPauseMenuWidget::HandleQuitToMenuClicked);
}
Super::NativeDestruct();
}
void UPauseMenuWidget::HandleSettingsClicked()
{
if (AFpsPlayerController* Controller = GetOwningPlayer<AFpsPlayerController>())
{
Controller->OpenSettings();
}
}
void UPauseMenuWidget::HandleResumeClicked()
{
if (AFpsPlayerController* Controller = GetOwningPlayer<AFpsPlayerController>())
{
Controller->ResumeGame();
}
}
void UPauseMenuWidget::HandleQuitToMenuClicked()
{
if (AFpsPlayerController* Controller = GetOwningPlayer<AFpsPlayerController>())
{
Controller->RequestQuitToMainMenu();
}
}
@@ -21,6 +21,11 @@ UScreenFadeComponent::UScreenFadeComponent()
// pour une valeur qui ne bouge qu'une seconde par mort serait du gaspillage.
PrimaryComponentTick.bCanEverTick = true;
PrimaryComponentTick.bStartWithTickEnabled = false;
// Le fondu doit continuer d'avancer quand le monde est fige : c'est le seul
// moyen de partir au noir depuis le menu de pause. Sans ce drapeau, l'ecran
// resterait bloque a mi-chemin et le delegue ne se declencherait jamais.
PrimaryComponentTick.bTickEvenWhenPaused = true;
}
void UScreenFadeComponent::BeginPlay()
@@ -0,0 +1,164 @@
// Fill out your copyright notice in the Description page of Project Settings.
#include "SettingRowWidget.h"
#include "Components/Button.h"
#include "Components/ProgressBar.h"
#include "Components/TextBlock.h"
void USettingRowWidget::NativeOnInitialized()
{
Super::NativeOnInitialized();
// NativeOnInitialized et non NativeConstruct : l'onglet parent appelle
// SetupAs... depuis SON NativeConstruct, et rien ne garantit l'ordre entre
// le construct du parent et celui de ses enfants. Initialize, lui, tourne
// une seule fois a la creation de l'arbre, avant tout le monde.
if (LeftArrowButton)
{
LeftArrowButton->OnClicked.AddDynamic(this, &USettingRowWidget::HandleLeftClicked);
}
if (RightArrowButton)
{
RightArrowButton->OnClicked.AddDynamic(this, &USettingRowWidget::HandleRightClicked);
}
}
void USettingRowWidget::SetupAsChoice(const FText& Label, const TArray<FText>& Options, int32 Index)
{
bNumeric = false;
ChoiceLabels = Options;
StepCount = FMath::Max(Options.Num(), 1);
CurrentIndex = FMath::Clamp(Index, 0, StepCount - 1);
if (LabelText)
{
LabelText->SetText(Label);
}
// Une barre de remplissage sur "Maintien / Bascule" ne veut rien dire : elle
// serait vide ou pleine sans que ca informe de quoi que ce soit. On la
// masque plutot que d'imposer un second WBP juste pour les choix.
if (ValueBar)
{
ValueBar->SetVisibility(ESlateVisibility::Collapsed);
}
Refresh();
}
void USettingRowWidget::SetupAsNumber(const FText& Label, float Min, float Max, float Step, float Value, const FText& Suffix)
{
bNumeric = true;
ChoiceLabels.Reset();
NumericMin = Min;
NumericStep = FMath::Max(Step, KINDA_SMALL_NUMBER);
NumericSuffix = Suffix;
// +1 : les deux extremites comptent. 70 a 120 par 5 fait 11 paliers, pas 10.
StepCount = FMath::Max(FMath::FloorToInt((Max - Min) / NumericStep) + 1, 1);
if (LabelText)
{
LabelText->SetText(Label);
}
// HitTestInvisible et non Visible : la barre est un decor, elle ne doit pas
// intercepter les clics destines aux fleches qui l'encadrent.
if (ValueBar)
{
ValueBar->SetVisibility(ESlateVisibility::HitTestInvisible);
}
SetNumericValueSilently(Value);
}
float USettingRowWidget::GetNumericValue() const
{
return NumericMin + CurrentIndex * NumericStep;
}
void USettingRowWidget::SetIndexSilently(int32 NewIndex)
{
CurrentIndex = FMath::Clamp(NewIndex, 0, StepCount - 1);
Refresh();
}
void USettingRowWidget::SetNumericValueSilently(float Value)
{
SetIndexSilently(FMath::RoundToInt((Value - NumericMin) / NumericStep));
}
void USettingRowWidget::HandleLeftClicked()
{
StepIndex(-1);
}
void USettingRowWidget::HandleRightClicked()
{
StepIndex(1);
}
void USettingRowWidget::StepIndex(int32 Delta)
{
const int32 NewIndex = FMath::Clamp(CurrentIndex + Delta, 0, StepCount - 1);
// Pas de bouclage, volontairement : passer de 100 a 5 de sensibilite d'un
// clic de trop rendrait le jeu injouable sans qu'on comprenne pourquoi.
if (NewIndex == CurrentIndex)
{
return;
}
CurrentIndex = NewIndex;
Refresh();
OnIndexChanged.Broadcast(CurrentIndex);
}
void USettingRowWidget::Refresh()
{
if (ValueText)
{
if (bNumeric)
{
FNumberFormattingOptions Format;
Format.MaximumFractionalDigits = 0;
const FText Number = FText::AsNumber(GetNumericValue(), &Format);
// FormatOrdered plutot qu'une concatenation : selon la langue, l'unite
// ne se colle pas forcement du meme cote ni avec la meme espace.
ValueText->SetText(NumericSuffix.IsEmpty()
? Number
: FText::FormatOrdered(NSLOCTEXT("Reglages", "ValeurAvecUnite", "{0}{1}"), Number, NumericSuffix));
}
else if (ChoiceLabels.IsValidIndex(CurrentIndex))
{
ValueText->SetText(ChoiceLabels[CurrentIndex]);
}
}
if (ValueBar)
{
// Un seul palier : la barre n'a aucun sens, on la remplit entierement
// plutot que de diviser par zero.
ValueBar->SetPercent(StepCount > 1
? static_cast<float>(CurrentIndex) / static_cast<float>(StepCount - 1)
: 1.f);
}
// Les fleches se grisent aux extremites : le joueur voit qu'il est au bout
// au lieu de cliquer dans le vide.
if (LeftArrowButton)
{
LeftArrowButton->SetIsEnabled(CurrentIndex > 0);
}
if (RightArrowButton)
{
RightArrowButton->SetIsEnabled(CurrentIndex < StepCount - 1);
}
}
@@ -0,0 +1,278 @@
// Fill out your copyright notice in the Description page of Project Settings.
#include "SettingsGameplayWidget.h"
#include "SettingRowWidget.h"
#include "SurvivalUserSettings.h"
#define LOCTEXT_NAMESPACE "Reglages"
namespace
{
/** Les deux seules listes de choix de cet onglet. */
TArray<FText> MakeNoYesLabels()
{
return { LOCTEXT("Non", "Non"), LOCTEXT("Oui", "Oui") };
}
TArray<FText> MakeHoldToggleLabels()
{
return { LOCTEXT("Maintien", "Maintien"), LOCTEXT("Bascule", "Bascule") };
}
}
void USettingsGameplayWidget::NativeConstruct()
{
Super::NativeConstruct();
// Les libelles et les bornes sont poses une seule fois ; seules les valeurs
// bougent ensuite, via RefreshFromSettings.
using namespace SurvivalSettingsRange;
if (SensitivityRow)
{
SensitivityRow->SetupAsNumber(LOCTEXT("Sensibilite", "Sensibilité"),
SensitivityMin, SensitivityMax, SensitivityStep, SensitivityMin, FText::GetEmpty());
SensitivityRow->OnIndexChanged.AddDynamic(this, &USettingsGameplayWidget::HandleSensitivityChanged);
}
if (SeparateVerticalRow)
{
SeparateVerticalRow->SetupAsChoice(LOCTEXT("SensibiliteSeparee", "Sensibilité verticale séparée"),
MakeNoYesLabels(), 0);
SeparateVerticalRow->OnIndexChanged.AddDynamic(this, &USettingsGameplayWidget::HandleSeparateVerticalChanged);
}
if (SensitivityVerticalRow)
{
SensitivityVerticalRow->SetupAsNumber(LOCTEXT("SensibiliteVerticale", "Sensibilité verticale"),
SensitivityMin, SensitivityMax, SensitivityStep, SensitivityMin, FText::GetEmpty());
SensitivityVerticalRow->OnIndexChanged.AddDynamic(this, &USettingsGameplayWidget::HandleSensitivityVerticalChanged);
}
if (InvertLookRow)
{
InvertLookRow->SetupAsChoice(LOCTEXT("InverserY", "Inverser l'axe vertical"), MakeNoYesLabels(), 0);
InvertLookRow->OnIndexChanged.AddDynamic(this, &USettingsGameplayWidget::HandleInvertLookChanged);
}
if (FieldOfViewRow)
{
FieldOfViewRow->SetupAsNumber(LOCTEXT("ChampDeVision", "Champ de vision"),
FieldOfViewMin, FieldOfViewMax, FieldOfViewStep, FieldOfViewMin, LOCTEXT("Degres", "°"));
FieldOfViewRow->OnIndexChanged.AddDynamic(this, &USettingsGameplayWidget::HandleFieldOfViewChanged);
}
if (CameraShakeRow)
{
CameraShakeRow->SetupAsNumber(LOCTEXT("BougeCamera", "Bougé de caméra"),
CameraShakeMin, CameraShakeMax, CameraShakeStep, CameraShakeMin, LOCTEXT("Pourcent", " %"));
CameraShakeRow->OnIndexChanged.AddDynamic(this, &USettingsGameplayWidget::HandleCameraShakeChanged);
}
if (SprintModeRow)
{
SprintModeRow->SetupAsChoice(LOCTEXT("Sprint", "Sprint"), MakeHoldToggleLabels(), 0);
SprintModeRow->OnIndexChanged.AddDynamic(this, &USettingsGameplayWidget::HandleSprintModeChanged);
}
if (CrouchModeRow)
{
CrouchModeRow->SetupAsChoice(LOCTEXT("Accroupi", "Accroupi"), MakeHoldToggleLabels(), 0);
CrouchModeRow->OnIndexChanged.AddDynamic(this, &USettingsGameplayWidget::HandleCrouchModeChanged);
}
RefreshFromSettings();
}
void USettingsGameplayWidget::RefreshFromSettings()
{
USurvivalUserSettings* Settings = USurvivalUserSettings::Get();
if (!Settings)
{
UE_LOG(LogTemp, Error, TEXT("USettingsGameplayWidget : GameUserSettingsClassName n'est pas configure dans DefaultEngine.ini, l'onglet restera inerte."));
return;
}
// SetNumericValueSilently et non un setter classique : diffuser ici ferait
// boucler l'onglet sur lui-meme.
if (SensitivityRow)
{
SensitivityRow->SetNumericValueSilently(Settings->GetLookSensitivity());
}
const bool bSeparate = Settings->GetSeparateVerticalSensitivity();
if (SeparateVerticalRow)
{
SeparateVerticalRow->SetIndexSilently(bSeparate ? 1 : 0);
}
if (SensitivityVerticalRow)
{
SensitivityVerticalRow->SetNumericValueSilently(Settings->GetLookSensitivityVertical());
}
UpdateVerticalRowVisibility(bSeparate);
if (InvertLookRow)
{
InvertLookRow->SetIndexSilently(Settings->GetInvertLookY() ? 1 : 0);
}
if (FieldOfViewRow)
{
FieldOfViewRow->SetNumericValueSilently(Settings->GetFieldOfView());
}
if (CameraShakeRow)
{
CameraShakeRow->SetNumericValueSilently(Settings->GetCameraShakeScale());
}
if (SprintModeRow)
{
SprintModeRow->SetIndexSilently(Settings->GetSprintToggle() ? 1 : 0);
}
if (CrouchModeRow)
{
CrouchModeRow->SetIndexSilently(Settings->GetCrouchToggle() ? 1 : 0);
}
}
void USettingsGameplayWidget::ResetToDefaults()
{
USurvivalUserSettings* Settings = USurvivalUserSettings::Get();
if (!Settings)
{
return;
}
Settings->SetSurvivalGameplayToDefaults();
PushToPawn(Settings);
RefreshFromSettings();
}
void USettingsGameplayWidget::UpdateVerticalRowVisibility(bool bSeparate)
{
if (SensitivityVerticalRow)
{
// Collapsed et non Hidden : la ligne ne doit pas laisser un trou dans la
// liste quand elle est inutile.
SensitivityVerticalRow->SetVisibility(bSeparate ? ESlateVisibility::Visible : ESlateVisibility::Collapsed);
}
}
void USettingsGameplayWidget::PushToPawn(USurvivalUserSettings* Settings)
{
// Pas de SaveSettings ici : ecrire le .ini a chaque clic de fleche est du
// gaspillage. C'est l'ecran de reglages qui sauvegarde en se fermant.
Settings->ApplySurvivalSettings();
}
void USettingsGameplayWidget::HandleSensitivityChanged(int32 NewIndex)
{
USurvivalUserSettings* Settings = USurvivalUserSettings::Get();
if (!Settings || !SensitivityRow)
{
return;
}
Settings->SetLookSensitivity(SensitivityRow->GetNumericValue());
// Sans axe separe, le vertical suit : on garde les deux lignes coherentes
// pour que le joueur ne decouvre pas un ecart en activant la separation.
if (!Settings->GetSeparateVerticalSensitivity() && SensitivityVerticalRow)
{
SensitivityVerticalRow->SetNumericValueSilently(Settings->GetLookSensitivityVertical());
}
PushToPawn(Settings);
}
void USettingsGameplayWidget::HandleSeparateVerticalChanged(int32 NewIndex)
{
USurvivalUserSettings* Settings = USurvivalUserSettings::Get();
if (!Settings)
{
return;
}
const bool bSeparate = NewIndex == 1;
Settings->SetSeparateVerticalSensitivity(bSeparate);
if (SensitivityVerticalRow)
{
SensitivityVerticalRow->SetNumericValueSilently(Settings->GetLookSensitivityVertical());
}
UpdateVerticalRowVisibility(bSeparate);
PushToPawn(Settings);
}
void USettingsGameplayWidget::HandleSensitivityVerticalChanged(int32 NewIndex)
{
USurvivalUserSettings* Settings = USurvivalUserSettings::Get();
if (!Settings || !SensitivityVerticalRow)
{
return;
}
Settings->SetLookSensitivityVertical(SensitivityVerticalRow->GetNumericValue());
PushToPawn(Settings);
}
void USettingsGameplayWidget::HandleInvertLookChanged(int32 NewIndex)
{
if (USurvivalUserSettings* Settings = USurvivalUserSettings::Get())
{
Settings->SetInvertLookY(NewIndex == 1);
PushToPawn(Settings);
}
}
void USettingsGameplayWidget::HandleFieldOfViewChanged(int32 NewIndex)
{
USurvivalUserSettings* Settings = USurvivalUserSettings::Get();
if (!Settings || !FieldOfViewRow)
{
return;
}
Settings->SetFieldOfView(FieldOfViewRow->GetNumericValue());
PushToPawn(Settings);
}
void USettingsGameplayWidget::HandleCameraShakeChanged(int32 NewIndex)
{
USurvivalUserSettings* Settings = USurvivalUserSettings::Get();
if (!Settings || !CameraShakeRow)
{
return;
}
Settings->SetCameraShakeScale(CameraShakeRow->GetNumericValue());
PushToPawn(Settings);
}
void USettingsGameplayWidget::HandleSprintModeChanged(int32 NewIndex)
{
if (USurvivalUserSettings* Settings = USurvivalUserSettings::Get())
{
Settings->SetSprintToggle(NewIndex == 1);
PushToPawn(Settings);
}
}
void USettingsGameplayWidget::HandleCrouchModeChanged(int32 NewIndex)
{
if (USurvivalUserSettings* Settings = USurvivalUserSettings::Get())
{
Settings->SetCrouchToggle(NewIndex == 1);
PushToPawn(Settings);
}
}
#undef LOCTEXT_NAMESPACE
@@ -0,0 +1,268 @@
// Fill out your copyright notice in the Description page of Project Settings.
#include "SettingsMenuWidget.h"
#include "Components/Button.h"
#include "Components/WidgetSwitcher.h"
#include "ConfirmDialogWidget.h"
#include "InputCoreTypes.h"
#include "SettingsTabWidget.h"
#include "SurvivalUserSettings.h"
#define LOCTEXT_NAMESPACE "Reglages"
namespace
{
// Ordre des pages dans le WidgetSwitcher. Les trois dernieres n'existent pas
// encore : leur bouton se grise tout seul tant que la page est absente.
constexpr int32 TabIndexGameplay = 0;
constexpr int32 TabIndexDisplay = 1;
constexpr int32 TabIndexGraphics = 2;
constexpr int32 TabIndexAudio = 3;
}
USettingsMenuWidget::USettingsMenuWidget(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
// Sans ce drapeau le widget ne peut pas prendre le focus clavier, et
// NativeOnKeyDown ne serait jamais appele. Il ne se change qu'a la
// construction, d'ou sa presence ici et pas dans Open().
SetIsFocusable(true);
ResetDialogMessage = LOCTEXT("ResetConfirm", "Réinitialiser cet onglet ?");
}
void USettingsMenuWidget::NativeConstruct()
{
Super::NativeConstruct();
if (BackButton)
{
BackButton->OnClicked.AddDynamic(this, &USettingsMenuWidget::HandleBackClicked);
}
if (ResetButton)
{
ResetButton->OnClicked.AddDynamic(this, &USettingsMenuWidget::HandleResetClicked);
}
if (GameplayTabButton)
{
GameplayTabButton->OnClicked.AddDynamic(this, &USettingsMenuWidget::HandleGameplayTabClicked);
}
if (DisplayTabButton)
{
DisplayTabButton->OnClicked.AddDynamic(this, &USettingsMenuWidget::HandleDisplayTabClicked);
}
if (GraphicsTabButton)
{
GraphicsTabButton->OnClicked.AddDynamic(this, &USettingsMenuWidget::HandleGraphicsTabClicked);
}
if (AudioTabButton)
{
AudioTabButton->OnClicked.AddDynamic(this, &USettingsMenuWidget::HandleAudioTabClicked);
}
if (ResetDialog)
{
ResetDialog->OnConfirmed.AddDynamic(this, &USettingsMenuWidget::HandleResetConfirmed);
ResetDialog->OnCancelled.AddDynamic(this, &USettingsMenuWidget::HandleResetCancelled);
ResetDialog->Close();
}
UpdateTabButtonStates();
}
void USettingsMenuWidget::NativeDestruct()
{
if (BackButton)
{
BackButton->OnClicked.RemoveDynamic(this, &USettingsMenuWidget::HandleBackClicked);
}
if (ResetButton)
{
ResetButton->OnClicked.RemoveDynamic(this, &USettingsMenuWidget::HandleResetClicked);
}
Super::NativeDestruct();
}
void USettingsMenuWidget::Open()
{
SetVisibility(ESlateVisibility::Visible);
// L'ecran a pu etre ouvert, referme, puis rouvert apres un changement fait
// ailleurs : on relit toujours plutot que de faire confiance a l'affichage.
if (USettingsTabWidget* ActiveTab = GetActiveTab())
{
ActiveTab->RefreshFromSettings();
}
// Le focus doit etre repris a CHAQUE ouverture, sinon Echap reste sans effet.
SetKeyboardFocus();
}
void USettingsMenuWidget::Close()
{
if (ResetDialog)
{
ResetDialog->Close();
}
bResetDialogOpen = false;
SetVisibility(ESlateVisibility::Collapsed);
// La sauvegarde a lieu ici et pas a chaque clic de fleche : ecrire le .ini
// vingt fois pendant qu'on cherche la bonne sensibilite ne sert a rien.
if (USurvivalUserSettings* Settings = USurvivalUserSettings::Get())
{
Settings->SaveSettings();
}
}
FReply USettingsMenuWidget::NativeOnKeyDown(const FGeometry& InGeometry, const FKeyEvent& InKeyEvent)
{
const FKey Key = InKeyEvent.GetKey();
if (Key == EKeys::Escape)
{
// La boite de confirmation gere Echap elle-meme quand elle a le focus.
// Si elle l'a perdu, on annule ici plutot que de fermer tout l'ecran en
// laissant la question affichee.
if (bResetDialogOpen)
{
HandleResetCancelled();
return FReply::Handled();
}
OnCloseRequested.Broadcast();
// Handled : sans ca la touche continue de remonter et le menu de pause
// la consommerait une seconde fois.
return FReply::Handled();
}
return Super::NativeOnKeyDown(InGeometry, InKeyEvent);
}
USettingsTabWidget* USettingsMenuWidget::GetActiveTab() const
{
return TabSwitcher ? Cast<USettingsTabWidget>(TabSwitcher->GetActiveWidget()) : nullptr;
}
void USettingsMenuWidget::ShowTab(int32 Index)
{
if (!TabSwitcher || Index < 0 || Index >= TabSwitcher->GetNumWidgets())
{
return;
}
TabSwitcher->SetActiveWidgetIndex(Index);
if (USettingsTabWidget* ActiveTab = GetActiveTab())
{
ActiveTab->RefreshFromSettings();
}
}
void USettingsMenuWidget::UpdateTabButtonStates()
{
const int32 PageCount = TabSwitcher ? TabSwitcher->GetNumWidgets() : 0;
if (GameplayTabButton)
{
GameplayTabButton->SetIsEnabled(PageCount > TabIndexGameplay);
}
if (DisplayTabButton)
{
DisplayTabButton->SetIsEnabled(PageCount > TabIndexDisplay);
}
if (GraphicsTabButton)
{
GraphicsTabButton->SetIsEnabled(PageCount > TabIndexGraphics);
}
if (AudioTabButton)
{
AudioTabButton->SetIsEnabled(PageCount > TabIndexAudio);
}
}
void USettingsMenuWidget::HandleBackClicked()
{
OnCloseRequested.Broadcast();
}
void USettingsMenuWidget::HandleResetClicked()
{
// Sans boite assignee on ne bloque pas le joueur : on reinitialise.
if (!ResetDialog)
{
HandleResetConfirmed();
return;
}
bResetDialogOpen = true;
ResetDialog->Open(ResetDialogMessage);
}
void USettingsMenuWidget::HandleResetConfirmed()
{
bResetDialogOpen = false;
if (ResetDialog)
{
ResetDialog->Close();
}
// Uniquement l'onglet visible : un "Reinitialiser" qui remettrait aussi la
// resolution et le volume serait une mauvaise surprise.
if (USettingsTabWidget* ActiveTab = GetActiveTab())
{
ActiveTab->ResetToDefaults();
}
SetKeyboardFocus();
}
void USettingsMenuWidget::HandleResetCancelled()
{
bResetDialogOpen = false;
if (ResetDialog)
{
ResetDialog->Close();
}
SetKeyboardFocus();
}
void USettingsMenuWidget::HandleGameplayTabClicked()
{
ShowTab(TabIndexGameplay);
}
void USettingsMenuWidget::HandleDisplayTabClicked()
{
ShowTab(TabIndexDisplay);
}
void USettingsMenuWidget::HandleGraphicsTabClicked()
{
ShowTab(TabIndexGraphics);
}
void USettingsMenuWidget::HandleAudioTabClicked()
{
ShowTab(TabIndexAudio);
}
#undef LOCTEXT_NAMESPACE
@@ -0,0 +1,27 @@
// Fill out your copyright notice in the Description page of Project Settings.
#include "SurvivalGameInstance.h"
#include "SurvivalUserSettings.h"
void USurvivalGameInstance::Init()
{
Super::Init();
// Les reglages sont deja LUS depuis le .ini par le moteur, mais rien ne les
// a encore APPLIQUES : sans cet appel, la resolution et la qualite
// choisies au dernier lancement resteraient lettre morte jusqu'au premier
// passage dans le menu.
//
// C'est le bon endroit : le GameInstance est le seul objet qui naisse avant
// la premiere map et survive a tous les changements de niveau.
if (USurvivalUserSettings* Settings = USurvivalUserSettings::Get())
{
Settings->ApplySettings(/*bCheckForCommandLineOverrides=*/false);
}
else
{
UE_LOG(LogTemp, Error, TEXT("USurvivalGameInstance : GameUserSettingsClassName n'est pas configure dans DefaultEngine.ini, aucun reglage ne sera pris en compte."));
}
}
@@ -0,0 +1,144 @@
// Fill out your copyright notice in the Description page of Project Settings.
#include "SurvivalUserSettings.h"
#include "Engine/Engine.h"
namespace
{
/** Arrondit sur le palier le plus proche, puis borne. */
float SnapToStep(float Value, float Min, float Max, float Step)
{
const float Snapped = Min + FMath::RoundToFloat((Value - Min) / Step) * Step;
return FMath::Clamp(Snapped, Min, Max);
}
}
USurvivalUserSettings::USurvivalUserSettings()
{
// Le constructeur tourne sur le CDO, avant toute lecture du .ini : ces
// valeurs sont donc bien les defauts d'usine, ecrasees ensuite par le
// fichier s'il existe.
SetSurvivalGameplayToDefaults();
}
USurvivalUserSettings* USurvivalUserSettings::Get()
{
return GEngine ? Cast<USurvivalUserSettings>(GEngine->GetGameUserSettings()) : nullptr;
}
void USurvivalUserSettings::SetSurvivalGameplayToDefaults()
{
LookSensitivity = 35.f;
bSeparateVerticalSensitivity = false;
LookSensitivityVertical = 35.f;
bInvertLookY = false;
FieldOfView = 90.f;
CameraShakeScale = 100.f;
bSprintToggle = false;
bCrouchToggle = false;
}
void USurvivalUserSettings::SetToDefaults()
{
// Le "Reinitialiser" global du moteur doit aussi remettre NOS valeurs :
// sans cette surcharge, l'onglet Jeu resterait seul inchange.
Super::SetToDefaults();
SetSurvivalGameplayToDefaults();
}
void USurvivalUserSettings::ApplySurvivalSettings()
{
OnSurvivalSettingsApplied.Broadcast();
}
void USurvivalUserSettings::ApplyNonResolutionSettings()
{
Super::ApplyNonResolutionSettings();
// ApplySettings() passe forcement par ici : brancher la diffusion a ce
// niveau garantit que nos reglages suivent, quel que soit le chemin.
ApplySurvivalSettings();
}
float USurvivalUserSettings::GetLookMultiplierX() const
{
using namespace SurvivalSettingsRange;
const float Normalized = FMath::GetRangePct(SensitivityMin, SensitivityMax, LookSensitivity);
return FMath::Lerp(LookMultiplierAtMin, LookMultiplierAtMax, Normalized);
}
float USurvivalUserSettings::GetLookMultiplierY() const
{
using namespace SurvivalSettingsRange;
// Sans axe separe, le vertical suit l'horizontal : c'est ce que veulent
// 95 % des joueurs, et deux curseurs desynchronises par inadvertance
// donnent une visee bancale sans qu'on comprenne pourquoi.
if (!bSeparateVerticalSensitivity)
{
return GetLookMultiplierX();
}
const float Normalized = FMath::GetRangePct(SensitivityMin, SensitivityMax, LookSensitivityVertical);
return FMath::Lerp(LookMultiplierAtMin, LookMultiplierAtMax, Normalized);
}
float USurvivalUserSettings::GetCameraShakeMultiplier() const
{
return FMath::Clamp(CameraShakeScale / 100.f, 0.f, 1.f);
}
void USurvivalUserSettings::SetLookSensitivity(float Value)
{
using namespace SurvivalSettingsRange;
LookSensitivity = SnapToStep(Value, SensitivityMin, SensitivityMax, SensitivityStep);
}
void USurvivalUserSettings::SetSeparateVerticalSensitivity(bool bValue)
{
// En repassant sur "non separe" on recale le vertical sur l'horizontal :
// sinon la valeur oubliee ressortirait telle quelle a la reactivation.
if (!bValue)
{
LookSensitivityVertical = LookSensitivity;
}
bSeparateVerticalSensitivity = bValue;
}
void USurvivalUserSettings::SetLookSensitivityVertical(float Value)
{
using namespace SurvivalSettingsRange;
LookSensitivityVertical = SnapToStep(Value, SensitivityMin, SensitivityMax, SensitivityStep);
}
void USurvivalUserSettings::SetInvertLookY(bool bValue)
{
bInvertLookY = bValue;
}
void USurvivalUserSettings::SetFieldOfView(float Value)
{
using namespace SurvivalSettingsRange;
FieldOfView = SnapToStep(Value, FieldOfViewMin, FieldOfViewMax, FieldOfViewStep);
}
void USurvivalUserSettings::SetCameraShakeScale(float Value)
{
using namespace SurvivalSettingsRange;
CameraShakeScale = SnapToStep(Value, CameraShakeMin, CameraShakeMax, CameraShakeStep);
}
void USurvivalUserSettings::SetSprintToggle(bool bValue)
{
bSprintToggle = bValue;
}
void USurvivalUserSettings::SetCrouchToggle(bool bValue)
{
bCrouchToggle = bValue;
}