(Feat) Add Main Menu + Music Manger
This commit is contained in:
@@ -33,6 +33,9 @@ void UConfirmDialogWidget::NativeConstruct()
|
||||
|
||||
void UConfirmDialogWidget::Open(const FText& InMessage)
|
||||
{
|
||||
// Une ouverture simple annule un rebours laisse par la precedente.
|
||||
StopCountdown();
|
||||
|
||||
if (MessageText)
|
||||
{
|
||||
MessageText->SetText(InMessage);
|
||||
@@ -45,11 +48,79 @@ void UConfirmDialogWidget::Open(const FText& InMessage)
|
||||
SetKeyboardFocus();
|
||||
}
|
||||
|
||||
void UConfirmDialogWidget::OpenWithCountdown(const FText& InMessageFormat, float Seconds)
|
||||
{
|
||||
StopCountdown();
|
||||
|
||||
CountdownMessageFormat = InMessageFormat;
|
||||
CountdownRemaining = FMath::Max(Seconds, 1.f);
|
||||
LastDisplayedSeconds = -1;
|
||||
|
||||
RefreshCountdownText();
|
||||
|
||||
SetVisibility(ESlateVisibility::Visible);
|
||||
SetKeyboardFocus();
|
||||
|
||||
CountdownTickerHandle = FTSTicker::GetCoreTicker().AddTicker(
|
||||
FTickerDelegate::CreateUObject(this, &UConfirmDialogWidget::TickCountdown));
|
||||
}
|
||||
|
||||
void UConfirmDialogWidget::Close()
|
||||
{
|
||||
StopCountdown();
|
||||
|
||||
SetVisibility(ESlateVisibility::Collapsed);
|
||||
}
|
||||
|
||||
void UConfirmDialogWidget::StopCountdown()
|
||||
{
|
||||
if (CountdownTickerHandle.IsValid())
|
||||
{
|
||||
FTSTicker::GetCoreTicker().RemoveTicker(CountdownTickerHandle);
|
||||
CountdownTickerHandle.Reset();
|
||||
}
|
||||
}
|
||||
|
||||
void UConfirmDialogWidget::RefreshCountdownText()
|
||||
{
|
||||
if (!MessageText)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// CeilToInt : on affiche "10" pendant la premiere seconde et "1" pendant la
|
||||
// derniere. Avec un arrondi classique on afficherait "0" une demi-seconde
|
||||
// avant que ca se declenche, ce qui donne l'impression d'un bug.
|
||||
const int32 Seconds = FMath::CeilToInt(CountdownRemaining);
|
||||
if (Seconds == LastDisplayedSeconds)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
LastDisplayedSeconds = Seconds;
|
||||
MessageText->SetText(FText::Format(CountdownMessageFormat, FText::AsNumber(Seconds)));
|
||||
}
|
||||
|
||||
bool UConfirmDialogWidget::TickCountdown(float DeltaTime)
|
||||
{
|
||||
CountdownRemaining -= DeltaTime;
|
||||
|
||||
if (CountdownRemaining > 0.f)
|
||||
{
|
||||
RefreshCountdownText();
|
||||
|
||||
// true : on veut etre rappele a la frame suivante.
|
||||
return true;
|
||||
}
|
||||
|
||||
// On coupe le ticker AVANT de diffuser : un abonne qui rouvre une boite
|
||||
// dans son handler doit pouvoir relancer un rebours.
|
||||
StopCountdown();
|
||||
OnCancelled.Broadcast();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
FReply UConfirmDialogWidget::NativeOnKeyDown(const FGeometry& InGeometry, const FKeyEvent& InKeyEvent)
|
||||
{
|
||||
const FKey Key = InKeyEvent.GetKey();
|
||||
@@ -64,6 +135,7 @@ FReply UConfirmDialogWidget::NativeOnKeyDown(const FGeometry& InGeometry, const
|
||||
// reellement branchee.
|
||||
if (bCancelOnEscape && (Key == EKeys::Escape || Key == EKeys::Virtual_Gamepad_Back.GetVirtualKey()))
|
||||
{
|
||||
StopCountdown();
|
||||
OnCancelled.Broadcast();
|
||||
|
||||
// Handled et non Unhandled : sans ca la touche continue de remonter et
|
||||
@@ -73,6 +145,7 @@ FReply UConfirmDialogWidget::NativeOnKeyDown(const FGeometry& InGeometry, const
|
||||
|
||||
if (bConfirmOnEnter && (Key == EKeys::Enter || Key == EKeys::Virtual_Gamepad_Accept.GetVirtualKey()))
|
||||
{
|
||||
StopCountdown();
|
||||
OnConfirmed.Broadcast();
|
||||
return FReply::Handled();
|
||||
}
|
||||
@@ -82,16 +155,26 @@ FReply UConfirmDialogWidget::NativeOnKeyDown(const FGeometry& InGeometry, const
|
||||
|
||||
void UConfirmDialogWidget::HandleConfirmClicked()
|
||||
{
|
||||
// Le joueur a repondu : le rebours n'a plus lieu d'etre, et le laisser
|
||||
// tourner ferait arriver un OnCancelled apres coup.
|
||||
StopCountdown();
|
||||
|
||||
OnConfirmed.Broadcast();
|
||||
}
|
||||
|
||||
void UConfirmDialogWidget::HandleCancelClicked()
|
||||
{
|
||||
StopCountdown();
|
||||
|
||||
OnCancelled.Broadcast();
|
||||
}
|
||||
|
||||
void UConfirmDialogWidget::NativeDestruct()
|
||||
{
|
||||
// Le ticker du moteur survit au widget : sans ce retrait, il rappellerait
|
||||
// une methode sur un objet detruit.
|
||||
StopCountdown();
|
||||
|
||||
if (ConfirmButton)
|
||||
{
|
||||
ConfirmButton->OnClicked.RemoveDynamic(this, &UConfirmDialogWidget::HandleConfirmClicked);
|
||||
|
||||
@@ -97,6 +97,10 @@ void AFpsPlayer::BeginPlay()
|
||||
{
|
||||
if (DefaultMappingContext)
|
||||
{
|
||||
// On ACTIVE le contexte ici, mais on ne le DECLARE pas comme
|
||||
// remappable : c'est USurvivalGameInstance qui s'en charge, parce
|
||||
// que le menu principal n'a pas de pawn et doit quand meme
|
||||
// afficher les touches.
|
||||
Subsystem->AddMappingContext(DefaultMappingContext, 0);
|
||||
}
|
||||
else
|
||||
|
||||
@@ -539,6 +539,12 @@ void AFpsPlayerController::OpenSettings()
|
||||
|
||||
SettingsMenu->OnCloseRequested.AddDynamic(this, &AFpsPlayerController::HandleSettingsCloseRequested);
|
||||
SettingsMenu->AddToViewport(ZOrderInGameSettingsMenu);
|
||||
|
||||
// L'ecran de reglages est partage avec le menu principal, dont
|
||||
// l'empilement n'a rien a voir avec celui du jeu : c'est a nous, qui
|
||||
// possedons la pile, de lui dire ou poser sa boite. +10 la place devant
|
||||
// lui sans empieter sur la boite d'abandon de partie a 200.
|
||||
SettingsMenu->SetDialogZOrder(ZOrderInGameSettingsMenu + 10);
|
||||
}
|
||||
|
||||
bSettingsOpen = true;
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
|
||||
#include "KeyCaptureWidget.h"
|
||||
|
||||
#include "Components/Button.h"
|
||||
#include "Components/TextBlock.h"
|
||||
|
||||
#define LOCTEXT_NAMESPACE "Reglages"
|
||||
|
||||
UKeyCaptureWidget::UKeyCaptureWidget(const FObjectInitializer& ObjectInitializer)
|
||||
: Super(ObjectInitializer)
|
||||
{
|
||||
// Sans ce drapeau le widget ne peut pas prendre le focus clavier, et
|
||||
// NativeOnKeyDown ne serait jamais appele -- le voile resterait affiche sans
|
||||
// jamais rien capturer. Meme piege que UConfirmDialogWidget.
|
||||
SetIsFocusable(true);
|
||||
|
||||
PromptFormat = LOCTEXT("CapturePrompt", "Appuie sur une touche pour « {0} »\nÉchap pour annuler");
|
||||
}
|
||||
|
||||
void UKeyCaptureWidget::NativeOnInitialized()
|
||||
{
|
||||
Super::NativeOnInitialized();
|
||||
|
||||
if (ConfirmButton)
|
||||
{
|
||||
ConfirmButton->OnClicked.AddDynamic(this, &UKeyCaptureWidget::HandleConfirmClicked);
|
||||
}
|
||||
|
||||
if (CancelButton)
|
||||
{
|
||||
CancelButton->OnClicked.AddDynamic(this, &UKeyCaptureWidget::HandleCancelClicked);
|
||||
}
|
||||
}
|
||||
|
||||
void UKeyCaptureWidget::BeginCapture(const FText& ActionLabel)
|
||||
{
|
||||
State = EKeyCaptureState::Capturing;
|
||||
|
||||
if (PromptText)
|
||||
{
|
||||
PromptText->SetText(FText::Format(PromptFormat, ActionLabel));
|
||||
}
|
||||
|
||||
UpdateButtonVisibility();
|
||||
|
||||
SetVisibility(ESlateVisibility::Visible);
|
||||
|
||||
// Le focus se reprend a CHAQUE ouverture : le refermer le rend a l'ecran de
|
||||
// reglages, et un voile visible mais sans focus ignorerait le clavier.
|
||||
SetKeyboardFocus();
|
||||
}
|
||||
|
||||
void UKeyCaptureWidget::ShowConflict(const FText& Message)
|
||||
{
|
||||
State = EKeyCaptureState::Conflict;
|
||||
|
||||
if (PromptText)
|
||||
{
|
||||
PromptText->SetText(Message);
|
||||
}
|
||||
|
||||
UpdateButtonVisibility();
|
||||
|
||||
// Le voile n'a pas bouge, mais le focus a pu partir sur un bouton qui vient
|
||||
// d'apparaitre : on le reprend pour qu'Echap continue de repondre.
|
||||
SetKeyboardFocus();
|
||||
}
|
||||
|
||||
void UKeyCaptureWidget::EndCapture()
|
||||
{
|
||||
State = EKeyCaptureState::Idle;
|
||||
|
||||
UpdateButtonVisibility();
|
||||
SetVisibility(ESlateVisibility::Collapsed);
|
||||
}
|
||||
|
||||
void UKeyCaptureWidget::UpdateButtonVisibility()
|
||||
{
|
||||
// Collapsed et non Hidden : replies, les boutons ne doivent pas reserver la
|
||||
// place qu'ils occuperont, sinon la consigne de capture serait decentree.
|
||||
const ESlateVisibility ButtonVisibility = State == EKeyCaptureState::Conflict
|
||||
? ESlateVisibility::Visible
|
||||
: ESlateVisibility::Collapsed;
|
||||
|
||||
if (ConfirmButton)
|
||||
{
|
||||
ConfirmButton->SetVisibility(ButtonVisibility);
|
||||
}
|
||||
|
||||
if (CancelButton)
|
||||
{
|
||||
CancelButton->SetVisibility(ButtonVisibility);
|
||||
}
|
||||
}
|
||||
|
||||
FReply UKeyCaptureWidget::NativeOnKeyDown(const FGeometry& InGeometry, const FKeyEvent& InKeyEvent)
|
||||
{
|
||||
const FKey Key = InKeyEvent.GetKey();
|
||||
|
||||
if (State == EKeyCaptureState::Conflict)
|
||||
{
|
||||
// Echap vaut Annuler, comme partout ailleurs dans les menus.
|
||||
if (Key == EKeys::Escape)
|
||||
{
|
||||
OnConflictAnswered.Broadcast(false);
|
||||
}
|
||||
|
||||
// Aucune autre touche n'est lue : on ne capture plus, on attend une
|
||||
// reponse. Sans ce garde-fou, appuyer sur une touche pour "repondre"
|
||||
// relancerait un remappage a l'insu du joueur.
|
||||
return FReply::Handled();
|
||||
}
|
||||
|
||||
return CaptureKey(Key);
|
||||
}
|
||||
|
||||
FReply UKeyCaptureWidget::NativeOnMouseButtonDown(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent)
|
||||
{
|
||||
if (State == EKeyCaptureState::Conflict)
|
||||
{
|
||||
// Les clics sur les boutons ne remontent pas jusqu'ici, ils sont traites
|
||||
// avant. Tout le reste est avale pour garder le voile modal.
|
||||
return FReply::Handled();
|
||||
}
|
||||
|
||||
// La souris passe par le meme chemin que le clavier : Souris 4 et Souris 5
|
||||
// sont des touches comme les autres pour Enhanced Input.
|
||||
return CaptureKey(InMouseEvent.GetEffectingButton());
|
||||
}
|
||||
|
||||
FReply UKeyCaptureWidget::CaptureKey(const FKey& Key)
|
||||
{
|
||||
if (Key == EKeys::Escape)
|
||||
{
|
||||
OnCaptureCancelled.Broadcast();
|
||||
return FReply::Handled();
|
||||
}
|
||||
|
||||
// Les touches de modification seules ne sont pas capturees : Enhanced Input
|
||||
// les traite comme des touches a part entiere, mais assigner "Maj" tout court
|
||||
// rendrait inutilisable toute combinaison la contenant.
|
||||
if (Key.IsModifierKey())
|
||||
{
|
||||
return FReply::Handled();
|
||||
}
|
||||
|
||||
// Un axe analogique n'est pas assignable a une action Digital : on ignore
|
||||
// plutot que de laisser MapPlayerKey echouer plus loin sans explication.
|
||||
if (Key.IsAxis1D() || Key.IsAxis2D() || Key.IsAxis3D())
|
||||
{
|
||||
return FReply::Handled();
|
||||
}
|
||||
|
||||
OnKeyCaptured.Broadcast(Key);
|
||||
|
||||
// Handled dans tous les cas : le voile est modal, rien ne doit passer a
|
||||
// travers pendant qu'il attend une touche.
|
||||
return FReply::Handled();
|
||||
}
|
||||
|
||||
void UKeyCaptureWidget::HandleConfirmClicked()
|
||||
{
|
||||
OnConflictAnswered.Broadcast(true);
|
||||
}
|
||||
|
||||
void UKeyCaptureWidget::HandleCancelClicked()
|
||||
{
|
||||
OnConflictAnswered.Broadcast(false);
|
||||
}
|
||||
|
||||
#undef LOCTEXT_NAMESPACE
|
||||
@@ -229,6 +229,10 @@ void AMainMenuPlayerController::OpenSettings()
|
||||
|
||||
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();
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
#include "MusicDirector.h"
|
||||
|
||||
#include "MusicSubsystem.h"
|
||||
|
||||
AMusicDirector::AMusicDirector()
|
||||
{
|
||||
PrimaryActorTick.bCanEverTick = false;
|
||||
}
|
||||
|
||||
void AMusicDirector::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
|
||||
UMusicSubsystem* Music = UMusicSubsystem::Get(this);
|
||||
if (!Music)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// La consigne d'abord : elle doit etre posee meme si cette map ne change pas
|
||||
// de playlist, puisque c'est elle qui vaudra pour toutes les scenes suivantes.
|
||||
Music->SetLevelChangeMode(LevelChangeMode);
|
||||
|
||||
if (Playlist)
|
||||
{
|
||||
Music->PlayPlaylist(Playlist);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,423 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
#include "MusicSubsystem.h"
|
||||
|
||||
#include "Components/AudioComponent.h"
|
||||
#include "Engine/Engine.h"
|
||||
#include "Engine/GameInstance.h"
|
||||
#include "Engine/World.h"
|
||||
#include "Kismet/GameplayStatics.h"
|
||||
#include "MusicPlaylistDataAsset.h"
|
||||
#include "Sound/SoundBase.h"
|
||||
#include "UObject/UObjectGlobals.h"
|
||||
|
||||
UMusicSubsystem* UMusicSubsystem::Get(const UObject* WorldContextObject)
|
||||
{
|
||||
if (!GEngine || !WorldContextObject)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::LogAndReturnNull);
|
||||
UGameInstance* GameInstance = World ? World->GetGameInstance() : nullptr;
|
||||
return GameInstance ? GameInstance->GetSubsystem<UMusicSubsystem>() : nullptr;
|
||||
}
|
||||
|
||||
void UMusicSubsystem::Initialize(FSubsystemCollectionBase& Collection)
|
||||
{
|
||||
Super::Initialize(Collection);
|
||||
|
||||
// Le seul endroit d'ou l'on voit passer un changement de scene : le lecteur
|
||||
// survit aux maps, donc aucun BeginPlay ni aucun EndPlay ne peut jouer ce
|
||||
// role -- et la map d'arrivee n'a justement pas a connaître le sujet.
|
||||
PostLoadMapHandle = FCoreUObjectDelegates::PostLoadMapWithWorld.AddUObject(this, &UMusicSubsystem::HandlePostLoadMap);
|
||||
}
|
||||
|
||||
void UMusicSubsystem::Deinitialize()
|
||||
{
|
||||
if (PostLoadMapHandle.IsValid())
|
||||
{
|
||||
FCoreUObjectDelegates::PostLoadMapWithWorld.Remove(PostLoadMapHandle);
|
||||
PostLoadMapHandle.Reset();
|
||||
}
|
||||
|
||||
CancelPendingGap();
|
||||
|
||||
// Fondu a zero : on quitte le jeu, plus personne n'ecoute.
|
||||
FadeOutAndRelease(MusicComponent, 0.f);
|
||||
if (FadingOutComponent)
|
||||
{
|
||||
FadingOutComponent->Stop();
|
||||
FadingOutComponent = nullptr;
|
||||
}
|
||||
|
||||
Super::Deinitialize();
|
||||
}
|
||||
|
||||
void UMusicSubsystem::PlayPlaylist(UMusicPlaylistDataAsset* Playlist, bool bForceRestart)
|
||||
{
|
||||
if (!Playlist || Playlist->Tracks.IsEmpty())
|
||||
{
|
||||
StopMusic();
|
||||
return;
|
||||
}
|
||||
|
||||
// Deja en cours : on la laisse tranquille. C'est tout l'interet de faire
|
||||
// pointer la map du menu et celle du jeu vers le meme asset -- le chargement
|
||||
// devient inaudible.
|
||||
if (CurrentPlaylist == Playlist && bIsPlaying && !bForceRestart)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CancelPendingGap();
|
||||
|
||||
// L'ancienne musique s'eteint sur sa propre voie pendant que la nouvelle
|
||||
// entre : un composant audio ne peut pas se croiser avec lui-meme.
|
||||
FadeOutAndRelease(MusicComponent, CurrentPlaylist ? CurrentPlaylist->FadeOutDuration : Playlist->FadeOutDuration);
|
||||
|
||||
CurrentPlaylist = Playlist;
|
||||
bShuffleEnabled = Playlist->bShuffle;
|
||||
LastPlayedTrackIndex = INDEX_NONE;
|
||||
bIsPlaying = true;
|
||||
|
||||
BuildPlayOrder();
|
||||
if (PlayOrder.IsEmpty())
|
||||
{
|
||||
// Que des cases vides dans la liste.
|
||||
bIsPlaying = false;
|
||||
return;
|
||||
}
|
||||
|
||||
PlayTrackAtCursor(Playlist->FadeInDuration);
|
||||
}
|
||||
|
||||
void UMusicSubsystem::StopMusic(float FadeOutDuration)
|
||||
{
|
||||
CancelPendingGap();
|
||||
|
||||
const float Duration = (FadeOutDuration >= 0.f)
|
||||
? FadeOutDuration
|
||||
: (CurrentPlaylist ? CurrentPlaylist->FadeOutDuration : 0.f);
|
||||
|
||||
FadeOutAndRelease(MusicComponent, Duration);
|
||||
|
||||
bIsPlaying = false;
|
||||
CurrentPlaylist = nullptr;
|
||||
PlayOrder.Reset();
|
||||
OrderCursor = INDEX_NONE;
|
||||
|
||||
OnMusicTrackChanged.Broadcast(nullptr);
|
||||
}
|
||||
|
||||
void UMusicSubsystem::SkipToNextTrack()
|
||||
{
|
||||
if (!bIsPlaying || !CurrentPlaylist)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CancelPendingGap();
|
||||
|
||||
// On LIBERE le composant au lieu de le reutiliser. Stop() et FadeOut()
|
||||
// rediffusent OnAudioFinished, et cette diffusion transite par le thread audio :
|
||||
// elle peut donc arriver une frame plus tard, soit APRES le demarrage du morceau
|
||||
// suivant, ce qui en sauterait un second. Un composant neuf, celui d'avant
|
||||
// detache, rend ce retour tardif sans effet.
|
||||
const float SkipFade = FMath::Min(CurrentPlaylist->FadeOutDuration, 1.f);
|
||||
FadeOutAndRelease(MusicComponent, SkipFade);
|
||||
|
||||
AdvanceAndPlay(SkipFade);
|
||||
}
|
||||
|
||||
void UMusicSubsystem::SetShuffleEnabled(bool bEnabled)
|
||||
{
|
||||
if (bShuffleEnabled == bEnabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bShuffleEnabled = bEnabled;
|
||||
|
||||
if (!CurrentPlaylist)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Le morceau en cours continue ; seule la SUITE change. On recale le curseur
|
||||
// dessus pour qu'en mode ordonne la lecture reprenne juste apres lui, et non
|
||||
// au debut de la liste.
|
||||
BuildPlayOrder();
|
||||
const int32 Found = PlayOrder.IndexOfByKey(LastPlayedTrackIndex);
|
||||
OrderCursor = (Found != INDEX_NONE) ? Found : 0;
|
||||
}
|
||||
|
||||
void UMusicSubsystem::SetMusicVolume(float NewVolume)
|
||||
{
|
||||
MusicVolume = FMath::Clamp(NewVolume, 0.f, 1.f);
|
||||
|
||||
if (MusicComponent)
|
||||
{
|
||||
// SetVolumeMultiplier est independant du fader utilise par FadeIn/FadeOut :
|
||||
// regler le volume au milieu d'un fondu ne l'interrompt pas.
|
||||
MusicComponent->SetVolumeMultiplier(GetEffectiveVolume());
|
||||
}
|
||||
}
|
||||
|
||||
USoundBase* UMusicSubsystem::GetCurrentTrack() const
|
||||
{
|
||||
return MusicComponent ? MusicComponent->Sound : nullptr;
|
||||
}
|
||||
|
||||
void UMusicSubsystem::HandleTrackFinished()
|
||||
{
|
||||
// N'est atteint que par une fin naturelle : toute coupure manuelle passe par
|
||||
// FadeOutAndRelease, qui detache le delegue avant d'arreter le son.
|
||||
if (!bIsPlaying || !CurrentPlaylist)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const float MinGap = FMath::Max(0.f, CurrentPlaylist->MinGapBetweenTracks);
|
||||
const float MaxGap = FMath::Max(MinGap, CurrentPlaylist->MaxGapBetweenTracks);
|
||||
const float Gap = FMath::FRandRange(MinGap, MaxGap);
|
||||
|
||||
if (Gap > KINDA_SMALL_NUMBER)
|
||||
{
|
||||
ScheduleGap(Gap);
|
||||
}
|
||||
else
|
||||
{
|
||||
AdvanceAndPlay(0.f);
|
||||
}
|
||||
}
|
||||
|
||||
void UMusicSubsystem::HandlePostLoadMap(UWorld* LoadedWorld)
|
||||
{
|
||||
// En PIE plusieurs instances peuvent charger des maps en parallele : on ne
|
||||
// reagit qu'a la nôtre.
|
||||
if (!LoadedWorld || LoadedWorld->GetGameInstance() != GetGameInstance())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (LevelChangeMode != EMusicLevelChangeMode::New || !bIsPlaying)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Volontairement apres le chargement et non avant : le nouveau morceau entre
|
||||
// en meme temps que la nouvelle scene s'affiche, pas pendant l'ecran de
|
||||
// chargement ou personne ne l'associe a quoi que ce soit.
|
||||
SkipToNextTrack();
|
||||
}
|
||||
|
||||
void UMusicSubsystem::BuildPlayOrder()
|
||||
{
|
||||
PlayOrder.Reset();
|
||||
OrderCursor = 0;
|
||||
|
||||
if (!CurrentPlaylist)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
PlayOrder.Reserve(CurrentPlaylist->Tracks.Num());
|
||||
for (int32 Index = 0; Index < CurrentPlaylist->Tracks.Num(); ++Index)
|
||||
{
|
||||
// Filtre en amont : une case vide ne doit jamais devenir un morceau a jouer,
|
||||
// sinon il faudrait la sauter au moment de la lecture -- et une liste
|
||||
// entierement vide y bouclerait a l'infini.
|
||||
if (CurrentPlaylist->Tracks[Index])
|
||||
{
|
||||
PlayOrder.Add(Index);
|
||||
}
|
||||
}
|
||||
|
||||
if (!bShuffleEnabled || PlayOrder.Num() < 2)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Fisher-Yates : chaque permutation equiprobable, une seule passe.
|
||||
for (int32 Index = PlayOrder.Num() - 1; Index > 0; --Index)
|
||||
{
|
||||
PlayOrder.Swap(Index, FMath::RandRange(0, Index));
|
||||
}
|
||||
|
||||
// Un nouveau cycle ne redemarre jamais sur le morceau qui vient de finir.
|
||||
// C'est la difference entre "aleatoire" et "aleatoire agreable a ecouter".
|
||||
if (PlayOrder[0] == LastPlayedTrackIndex)
|
||||
{
|
||||
PlayOrder.Swap(0, FMath::RandRange(1, PlayOrder.Num() - 1));
|
||||
}
|
||||
}
|
||||
|
||||
void UMusicSubsystem::AdvanceAndPlay(float FadeInDuration)
|
||||
{
|
||||
if (!CurrentPlaylist)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
++OrderCursor;
|
||||
|
||||
if (!PlayOrder.IsValidIndex(OrderCursor))
|
||||
{
|
||||
if (!CurrentPlaylist->bLoop)
|
||||
{
|
||||
StopMusic(CurrentPlaylist->FadeOutDuration);
|
||||
return;
|
||||
}
|
||||
|
||||
BuildPlayOrder();
|
||||
if (PlayOrder.IsEmpty())
|
||||
{
|
||||
StopMusic(0.f);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
PlayTrackAtCursor(FadeInDuration);
|
||||
}
|
||||
|
||||
void UMusicSubsystem::PlayTrackAtCursor(float FadeInDuration)
|
||||
{
|
||||
if (!CurrentPlaylist || !PlayOrder.IsValidIndex(OrderCursor))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const int32 TrackIndex = PlayOrder[OrderCursor];
|
||||
USoundBase* Track = CurrentPlaylist->Tracks.IsValidIndex(TrackIndex) ? CurrentPlaylist->Tracks[TrackIndex].Get() : nullptr;
|
||||
if (!Track)
|
||||
{
|
||||
// La playlist a ete editee pendant la lecture : on reconstruit plutot que
|
||||
// de jouer un index perime.
|
||||
BuildPlayOrder();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!MusicComponent)
|
||||
{
|
||||
MusicComponent = CreateMusicComponent(Track);
|
||||
if (!MusicComponent)
|
||||
{
|
||||
bIsPlaying = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
MusicComponent->SetSound(Track);
|
||||
}
|
||||
|
||||
MusicComponent->SetVolumeMultiplier(GetEffectiveVolume());
|
||||
|
||||
if (FadeInDuration > 0.f)
|
||||
{
|
||||
MusicComponent->FadeIn(FadeInDuration, 1.f);
|
||||
}
|
||||
else
|
||||
{
|
||||
MusicComponent->Play();
|
||||
}
|
||||
|
||||
LastPlayedTrackIndex = TrackIndex;
|
||||
OnMusicTrackChanged.Broadcast(Track);
|
||||
}
|
||||
|
||||
UAudioComponent* UMusicSubsystem::CreateMusicComponent(USoundBase* Sound)
|
||||
{
|
||||
const UGameInstance* GameInstance = GetGameInstance();
|
||||
UWorld* World = GameInstance ? GameInstance->GetWorld() : nullptr;
|
||||
if (!World || !Sound)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// bPersistAcrossLevelTransition : le composant est cree sans monde ni acteur
|
||||
// proprietaire et marque bIgnoreForFlushing, le seul moyen de traverser un
|
||||
// OpenLevel sans que le peripherique audio ne le coupe.
|
||||
// bAutoDestroy a faux : on veut le reutiliser d'un morceau au suivant, et
|
||||
// surtout recevoir son OnAudioFinished plutot que de le voir disparaître.
|
||||
UAudioComponent* Component = UGameplayStatics::CreateSound2D(
|
||||
World, Sound,
|
||||
/*VolumeMultiplier*/ 1.f, /*PitchMultiplier*/ 1.f, /*StartTime*/ 0.f,
|
||||
/*ConcurrencySettings*/ nullptr,
|
||||
/*bPersistAcrossLevelTransition*/ true,
|
||||
/*bAutoDestroy*/ false);
|
||||
|
||||
if (!Component)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Component->bIsMusic = true;
|
||||
Component->OnAudioFinished.AddDynamic(this, &UMusicSubsystem::HandleTrackFinished);
|
||||
return Component;
|
||||
}
|
||||
|
||||
void UMusicSubsystem::FadeOutAndRelease(TObjectPtr<UAudioComponent>& Component, float FadeOutDuration)
|
||||
{
|
||||
if (!Component)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Detacher AVANT d'arreter : Stop() comme FadeOut() finissent par rediffuser
|
||||
// OnAudioFinished, qui enchainerait sur le morceau suivant alors qu'on cherche
|
||||
// justement a couper.
|
||||
Component->OnAudioFinished.RemoveDynamic(this, &UMusicSubsystem::HandleTrackFinished);
|
||||
|
||||
// Une seule voie de sortie : si un croisement precedent n'est pas fini, il est
|
||||
// coupe net. Empiler les fondus ferait monter le nombre de voix pour rien.
|
||||
if (FadingOutComponent && FadingOutComponent != Component)
|
||||
{
|
||||
FadingOutComponent->Stop();
|
||||
FadingOutComponent = nullptr;
|
||||
}
|
||||
|
||||
if (FadeOutDuration > 0.f && Component->IsPlaying())
|
||||
{
|
||||
Component->FadeOut(FadeOutDuration, 0.f);
|
||||
FadingOutComponent = Component;
|
||||
}
|
||||
else
|
||||
{
|
||||
Component->Stop();
|
||||
}
|
||||
|
||||
Component = nullptr;
|
||||
}
|
||||
|
||||
void UMusicSubsystem::ScheduleGap(float DelaySeconds)
|
||||
{
|
||||
CancelPendingGap();
|
||||
|
||||
TWeakObjectPtr<UMusicSubsystem> WeakThis(this);
|
||||
GapTickerHandle = FTSTicker::GetCoreTicker().AddTicker(TEXT("MusicSubsystem.Gap"), DelaySeconds,
|
||||
[WeakThis](float /*DeltaTime*/)
|
||||
{
|
||||
if (UMusicSubsystem* Self = WeakThis.Get())
|
||||
{
|
||||
Self->GapTickerHandle.Reset();
|
||||
Self->AdvanceAndPlay(0.f);
|
||||
}
|
||||
return false; // un seul declenchement
|
||||
});
|
||||
}
|
||||
|
||||
void UMusicSubsystem::CancelPendingGap()
|
||||
{
|
||||
if (GapTickerHandle.IsValid())
|
||||
{
|
||||
FTSTicker::RemoveTicker(GapTickerHandle);
|
||||
GapTickerHandle.Reset();
|
||||
}
|
||||
}
|
||||
|
||||
float UMusicSubsystem::GetEffectiveVolume() const
|
||||
{
|
||||
return MusicVolume * (CurrentPlaylist ? CurrentPlaylist->VolumeMultiplier : 1.f);
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
|
||||
#include "SettingKeyRowWidget.h"
|
||||
|
||||
#include "Components/Button.h"
|
||||
#include "Components/TextBlock.h"
|
||||
|
||||
#define LOCTEXT_NAMESPACE "Reglages"
|
||||
|
||||
void USettingKeyRowWidget::NativeOnInitialized()
|
||||
{
|
||||
Super::NativeOnInitialized();
|
||||
|
||||
// Comme USettingRowWidget : l'onglet appelle Setup depuis SON NativeConstruct,
|
||||
// et rien ne garantit l'ordre entre le construct du parent et celui de ses
|
||||
// enfants. Initialize, lui, tourne une fois a la creation de l'arbre.
|
||||
if (KeyButton)
|
||||
{
|
||||
KeyButton->OnClicked.AddDynamic(this, &USettingKeyRowWidget::HandleKeyButtonClicked);
|
||||
}
|
||||
}
|
||||
|
||||
void USettingKeyRowWidget::Setup(FName InMappingName, const FText& Label, const FKey& Key)
|
||||
{
|
||||
MappingName = InMappingName;
|
||||
|
||||
if (LabelText)
|
||||
{
|
||||
LabelText->SetText(Label);
|
||||
}
|
||||
|
||||
SetKey(Key);
|
||||
}
|
||||
|
||||
void USettingKeyRowWidget::SetKey(const FKey& Key)
|
||||
{
|
||||
if (!KeyText)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Une touche invalide n'est pas une erreur : c'est l'etat normal d'une action
|
||||
// dont on vient de voler la touche. Le rouge est le signal qu'il reste
|
||||
// quelque chose a reparer avant de quitter l'ecran.
|
||||
const bool bBound = Key.IsValid();
|
||||
|
||||
KeyText->SetText(bBound
|
||||
? Key.GetDisplayName()
|
||||
: LOCTEXT("ToucheNonAssignee", "Non assignée"));
|
||||
|
||||
KeyText->SetColorAndOpacity(FSlateColor(bBound ? BoundColor : UnboundColor));
|
||||
}
|
||||
|
||||
void USettingKeyRowWidget::HandleKeyButtonClicked()
|
||||
{
|
||||
// On ne fait rien de plus que transmettre : la ligne ignore ce qu'est un
|
||||
// profil de touches, et c'est tres bien ainsi.
|
||||
OnRebindRequested.Broadcast(MappingName);
|
||||
}
|
||||
|
||||
#undef LOCTEXT_NAMESPACE
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
#include "Components/Button.h"
|
||||
#include "Components/ProgressBar.h"
|
||||
#include "Components/Slider.h"
|
||||
#include "Components/TextBlock.h"
|
||||
|
||||
void USettingRowWidget::NativeOnInitialized()
|
||||
@@ -24,6 +25,20 @@ void USettingRowWidget::NativeOnInitialized()
|
||||
{
|
||||
RightArrowButton->OnClicked.AddDynamic(this, &USettingRowWidget::HandleRightClicked);
|
||||
}
|
||||
|
||||
if (ValueSlider)
|
||||
{
|
||||
ValueSlider->OnValueChanged.AddDynamic(this, &USettingRowWidget::HandleSliderValueChanged);
|
||||
}
|
||||
|
||||
// La valeur est POSEE SUR le curseur : si elle restait cliquable, elle
|
||||
// avalerait le clic au milieu de la course et le glissement ne partirait
|
||||
// jamais. Force ici plutot que dans le WBP : c'est le genre de case qu'on
|
||||
// oublie et dont le symptome (une zone morte au centre) est illisible.
|
||||
if (ValueText)
|
||||
{
|
||||
ValueText->SetVisibility(ESlateVisibility::HitTestInvisible);
|
||||
}
|
||||
}
|
||||
|
||||
void USettingRowWidget::SetupAsChoice(const FText& Label, const TArray<FText>& Options, int32 Index)
|
||||
@@ -31,6 +46,8 @@ void USettingRowWidget::SetupAsChoice(const FText& Label, const TArray<FText>& O
|
||||
bNumeric = false;
|
||||
ChoiceLabels = Options;
|
||||
StepCount = FMath::Max(Options.Num(), 1);
|
||||
ArrowIndexDelta = 1;
|
||||
MaxSelectableIndex = INDEX_NONE;
|
||||
CurrentIndex = FMath::Clamp(Index, 0, StepCount - 1);
|
||||
|
||||
if (LabelText)
|
||||
@@ -38,18 +55,22 @@ void USettingRowWidget::SetupAsChoice(const FText& Label, const TArray<FText>& O
|
||||
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.
|
||||
// Une jauge sur "Maintien / Bascule" ne veut rien dire : deux positions et
|
||||
// rien a doser. On replie l'empilement plutot que d'imposer un second WBP.
|
||||
if (ValueBar)
|
||||
{
|
||||
ValueBar->SetVisibility(ESlateVisibility::Collapsed);
|
||||
}
|
||||
|
||||
if (ValueSlider)
|
||||
{
|
||||
ValueSlider->SetVisibility(ESlateVisibility::Collapsed);
|
||||
}
|
||||
|
||||
Refresh();
|
||||
}
|
||||
|
||||
void USettingRowWidget::SetupAsNumber(const FText& Label, float Min, float Max, float Step, float Value, const FText& Suffix)
|
||||
void USettingRowWidget::SetupAsNumber(const FText& Label, float Min, float Max, float Step, float ArrowStep, float Value, const FText& Suffix)
|
||||
{
|
||||
bNumeric = true;
|
||||
ChoiceLabels.Reset();
|
||||
@@ -57,21 +78,39 @@ void USettingRowWidget::SetupAsNumber(const FText& Label, float Min, float Max,
|
||||
NumericStep = FMath::Max(Step, KINDA_SMALL_NUMBER);
|
||||
NumericSuffix = Suffix;
|
||||
|
||||
// +1 : les deux extremites comptent. 70 a 120 par 5 fait 11 paliers, pas 10.
|
||||
// +1 : les deux extremites comptent. 70 a 120 par 1 fait 51 paliers, pas 50.
|
||||
StepCount = FMath::Max(FMath::FloorToInt((Max - Min) / NumericStep) + 1, 1);
|
||||
|
||||
// Le bond des fleches ne vit qu'en nombre de paliers fins : tout le reste du
|
||||
// widget raisonne en index, y compris le curseur.
|
||||
ArrowIndexDelta = FMath::Max(FMath::RoundToInt(ArrowStep / NumericStep), 1);
|
||||
MaxSelectableIndex = INDEX_NONE;
|
||||
|
||||
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.
|
||||
// HitTestInvisible : le remplissage est un decor pose SOUS le curseur, il ne
|
||||
// doit surtout pas intercepter le clic qui devrait lancer le glissement.
|
||||
if (ValueBar)
|
||||
{
|
||||
ValueBar->SetVisibility(ESlateVisibility::HitTestInvisible);
|
||||
}
|
||||
|
||||
if (ValueSlider)
|
||||
{
|
||||
ValueSlider->SetVisibility(ESlateVisibility::Visible);
|
||||
|
||||
// La course du curseur est graduee en INDEX, pas dans l'unite du
|
||||
// reglage : 0 a 10 pour un FOV de 70 a 120 par pas de 5. Le glissement
|
||||
// ne peut donc produire qu'un palier valide, et le meme curseur sert a
|
||||
// n'importe quelle plage sans conversion.
|
||||
ValueSlider->SetMinValue(0.f);
|
||||
ValueSlider->SetMaxValue(static_cast<float>(FMath::Max(StepCount - 1, 1)));
|
||||
ValueSlider->SetStepSize(1.f);
|
||||
}
|
||||
|
||||
SetNumericValueSilently(Value);
|
||||
}
|
||||
|
||||
@@ -82,10 +121,25 @@ float USettingRowWidget::GetNumericValue() const
|
||||
|
||||
void USettingRowWidget::SetIndexSilently(int32 NewIndex)
|
||||
{
|
||||
// Borne sur StepCount et non sur MaxSelectable : le code a le droit
|
||||
// d'afficher un etat que le joueur ne peut pas choisir.
|
||||
CurrentIndex = FMath::Clamp(NewIndex, 0, StepCount - 1);
|
||||
Refresh();
|
||||
}
|
||||
|
||||
void USettingRowWidget::SetMaxSelectableIndex(int32 InMaxSelectableIndex)
|
||||
{
|
||||
MaxSelectableIndex = InMaxSelectableIndex;
|
||||
Refresh();
|
||||
}
|
||||
|
||||
int32 USettingRowWidget::MaxSelectable() const
|
||||
{
|
||||
return MaxSelectableIndex == INDEX_NONE
|
||||
? StepCount - 1
|
||||
: FMath::Clamp(MaxSelectableIndex, 0, StepCount - 1);
|
||||
}
|
||||
|
||||
void USettingRowWidget::SetNumericValueSilently(float Value)
|
||||
{
|
||||
SetIndexSilently(FMath::RoundToInt((Value - NumericMin) / NumericStep));
|
||||
@@ -93,17 +147,57 @@ void USettingRowWidget::SetNumericValueSilently(float Value)
|
||||
|
||||
void USettingRowWidget::HandleLeftClicked()
|
||||
{
|
||||
StepIndex(-1);
|
||||
StepIndex(ArrowTargetIndex(-1) - CurrentIndex);
|
||||
}
|
||||
|
||||
void USettingRowWidget::HandleRightClicked()
|
||||
{
|
||||
StepIndex(1);
|
||||
StepIndex(ArrowTargetIndex(1) - CurrentIndex);
|
||||
}
|
||||
|
||||
int32 USettingRowWidget::ArrowTargetIndex(int32 Direction) const
|
||||
{
|
||||
// Pas de bond a arrondir : une ligne de choix, ou un reglage dont les deux
|
||||
// pas sont identiques.
|
||||
if (ArrowIndexDelta <= 1)
|
||||
{
|
||||
return CurrentIndex + Direction;
|
||||
}
|
||||
|
||||
// On vise le multiple du bond suivant, pas CurrentIndex + bond : arrive a 37
|
||||
// au curseur, la fleche droite doit donner 40 et la gauche 35. Sinon les
|
||||
// fleches perdent leur reperage des qu'on a glisse une fois.
|
||||
if (Direction > 0)
|
||||
{
|
||||
return ((CurrentIndex / ArrowIndexDelta) + 1) * ArrowIndexDelta;
|
||||
}
|
||||
|
||||
// Division entiere arrondie au superieur, pour que 35 redescende bien a 30
|
||||
// et pas a 35 (ce qui n'aurait rien fait).
|
||||
return ((CurrentIndex + ArrowIndexDelta - 1) / ArrowIndexDelta - 1) * ArrowIndexDelta;
|
||||
}
|
||||
|
||||
void USettingRowWidget::HandleSliderValueChanged(float NewValue)
|
||||
{
|
||||
if (bRefreshingSlider)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// On passe par StepIndex plutot que d'ecrire CurrentIndex : le glissement
|
||||
// et les fleches doivent produire exactement le meme effet, y compris le
|
||||
// grisage des fleches et la diffusion du delegue.
|
||||
StepIndex(FMath::RoundToInt(NewValue) - CurrentIndex);
|
||||
|
||||
// Puis on recale la poignee sur le cran, meme si l'index n'a pas bouge :
|
||||
// StepIndex sort sans rien faire dans ce cas, et la poignee resterait
|
||||
// plantee entre deux paliers.
|
||||
Refresh();
|
||||
}
|
||||
|
||||
void USettingRowWidget::StepIndex(int32 Delta)
|
||||
{
|
||||
const int32 NewIndex = FMath::Clamp(CurrentIndex + Delta, 0, StepCount - 1);
|
||||
const int32 NewIndex = FMath::Clamp(CurrentIndex + Delta, 0, MaxSelectable());
|
||||
|
||||
// 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.
|
||||
@@ -143,13 +237,22 @@ void USettingRowWidget::Refresh()
|
||||
|
||||
if (ValueBar)
|
||||
{
|
||||
// Un seul palier : la barre n'a aucun sens, on la remplit entierement
|
||||
// Un seul palier : la jauge 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);
|
||||
}
|
||||
|
||||
if (ValueSlider && bNumeric)
|
||||
{
|
||||
// Le drapeau evite qu'un repositionnement venant du code soit relu comme
|
||||
// un geste du joueur.
|
||||
bRefreshingSlider = true;
|
||||
ValueSlider->SetValue(static_cast<float>(CurrentIndex));
|
||||
bRefreshingSlider = false;
|
||||
}
|
||||
|
||||
// Les fleches se grisent aux extremites : le joueur voit qu'il est au bout
|
||||
// au lieu de cliquer dans le vide.
|
||||
if (LeftArrowButton)
|
||||
@@ -159,6 +262,6 @@ void USettingRowWidget::Refresh()
|
||||
|
||||
if (RightArrowButton)
|
||||
{
|
||||
RightArrowButton->SetIsEnabled(CurrentIndex < StepCount - 1);
|
||||
RightArrowButton->SetIsEnabled(CurrentIndex < MaxSelectable());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,344 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
|
||||
#include "SettingsControlsWidget.h"
|
||||
|
||||
#include "EnhancedInputSubsystems.h"
|
||||
#include "Engine/LocalPlayer.h"
|
||||
#include "GameFramework/PlayerController.h"
|
||||
#include "SettingKeyRowWidget.h"
|
||||
#include "SettingsMenuWidget.h"
|
||||
#include "SurvivalGameInstance.h"
|
||||
#include "UserSettings/EnhancedInputUserSettings.h"
|
||||
|
||||
#define LOCTEXT_NAMESPACE "Reglages"
|
||||
|
||||
USettingsControlsWidget::USettingsControlsWidget(const FObjectInitializer& ObjectInitializer)
|
||||
: Super(ObjectInitializer)
|
||||
{
|
||||
// Dans le CONSTRUCTEUR et non dans NativeConstruct : ce dernier tourne a
|
||||
// chaque ouverture et ecraserait le texte que tu aurais regle dans les Class
|
||||
// Defaults de WBP_SettingsControls, rendant la UPROPERTY inutile.
|
||||
//
|
||||
// Trois informations, pas deux : la touche, qui la possede aujourd'hui, et a
|
||||
// qui on la donne. Sans la troisieme, un joueur qui a ouvert le voile puis
|
||||
// hesite ne sait plus quelle ligne il etait en train de modifier.
|
||||
ConflictMessage = LOCTEXT("ConflitTouche",
|
||||
"« {0} » est déjà utilisée par « {1} ».\nL'assigner à « {2} » ?\n\n« {1} » n'aura plus de touche.");
|
||||
}
|
||||
|
||||
void USettingsControlsWidget::NativeConstruct()
|
||||
{
|
||||
Super::NativeConstruct();
|
||||
|
||||
// Les identifiants doivent correspondre EXACTEMENT au champ "Name" des
|
||||
// Player Mappable Key Settings de IMC_Default. Une faute de frappe donne une
|
||||
// ligne perpetuellement "Non assignée" -- c'est le seul point de contact
|
||||
// entre le C++ et l'editeur, et il n'est verifiable qu'au runtime.
|
||||
SetupRow(MoveForwardRow, TEXT("MoveForward"), LOCTEXT("Avancer", "Avancer"));
|
||||
SetupRow(MoveBackwardRow, TEXT("MoveBackward"), LOCTEXT("Reculer", "Reculer"));
|
||||
SetupRow(MoveLeftRow, TEXT("MoveLeft"), LOCTEXT("AllerGauche", "Aller à gauche"));
|
||||
SetupRow(MoveRightRow, TEXT("MoveRight"), LOCTEXT("AllerDroite", "Aller à droite"));
|
||||
SetupRow(JumpRow, TEXT("Jump"), LOCTEXT("Sauter", "Sauter"));
|
||||
SetupRow(SprintRow, TEXT("Sprint"), LOCTEXT("Sprinter", "Sprinter"));
|
||||
SetupRow(CrouchRow, TEXT("Crouch"), LOCTEXT("SAccroupir", "S'accroupir"));
|
||||
SetupRow(InteractRow, TEXT("Interact"), LOCTEXT("Interagir", "Interagir"));
|
||||
SetupRow(UseItemRow, TEXT("UseItem"), LOCTEXT("UtiliserObjet", "Utiliser l'objet"));
|
||||
SetupRow(ToggleInventoryRow, TEXT("ToggleInventory"), LOCTEXT("Inventaire", "Inventaire"));
|
||||
SetupRow(DropItemRow, TEXT("DropItem"), LOCTEXT("Jeter", "Jeter"));
|
||||
|
||||
RefreshFromSettings();
|
||||
}
|
||||
|
||||
void USettingsControlsWidget::SetupRow(USettingKeyRowWidget* Row, FName MappingName, const FText& Label)
|
||||
{
|
||||
if (!Row)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
MappingLabels.Add(MappingName, Label);
|
||||
|
||||
// Touche invalide au depart : RefreshFromSettings ira chercher la vraie juste
|
||||
// apres. Poser une valeur en dur ici mentirait le temps d'une frame.
|
||||
Row->Setup(MappingName, Label, FKey());
|
||||
|
||||
// Un seul handler pour les onze lignes : chacune renvoie son propre nom de
|
||||
// mapping, il n'y a rien a distinguer ici.
|
||||
Row->OnRebindRequested.AddDynamic(this, &USettingsControlsWidget::HandleRebindRequested);
|
||||
}
|
||||
|
||||
UEnhancedInputUserSettings* USettingsControlsWidget::GetInputSettings() const
|
||||
{
|
||||
const ULocalPlayer* LocalPlayer = GetOwningLocalPlayer();
|
||||
if (!LocalPlayer)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
UEnhancedInputLocalPlayerSubsystem* Subsystem = LocalPlayer->GetSubsystem<UEnhancedInputLocalPlayerSubsystem>();
|
||||
return Subsystem ? Subsystem->GetUserSettings() : nullptr;
|
||||
}
|
||||
|
||||
TArray<USettingKeyRowWidget*> USettingsControlsWidget::GetAllRows() const
|
||||
{
|
||||
// L'ordre est celui de l'affichage : il sert aussi a la reinitialisation.
|
||||
TArray<USettingKeyRowWidget*> Rows;
|
||||
Rows.Reserve(11);
|
||||
|
||||
for (USettingKeyRowWidget* Row : { MoveForwardRow.Get(), MoveBackwardRow.Get(), MoveLeftRow.Get(),
|
||||
MoveRightRow.Get(), JumpRow.Get(), SprintRow.Get(), CrouchRow.Get(), InteractRow.Get(),
|
||||
UseItemRow.Get(), ToggleInventoryRow.Get(), DropItemRow.Get() })
|
||||
{
|
||||
if (Row)
|
||||
{
|
||||
Rows.Add(Row);
|
||||
}
|
||||
}
|
||||
|
||||
return Rows;
|
||||
}
|
||||
|
||||
USettingKeyRowWidget* USettingsControlsWidget::FindRow(FName MappingName) const
|
||||
{
|
||||
for (USettingKeyRowWidget* Row : GetAllRows())
|
||||
{
|
||||
if (Row->GetMappingName() == MappingName)
|
||||
{
|
||||
return Row;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
FText USettingsControlsWidget::GetMappingDisplayName(FName MappingName) const
|
||||
{
|
||||
if (const FText* Label = MappingLabels.Find(MappingName))
|
||||
{
|
||||
return *Label;
|
||||
}
|
||||
|
||||
// Repli sur l'identifiant technique : moins joli, mais toujours plus utile
|
||||
// qu'un libelle vide dans un message de conflit.
|
||||
return FText::FromName(MappingName);
|
||||
}
|
||||
|
||||
void USettingsControlsWidget::RefreshFromSettings()
|
||||
{
|
||||
// On declare les contextes JUSTE AVANT de lire : c'est le seul moment ou
|
||||
// l'on est certain que le joueur local a son PlayerController, donc que le
|
||||
// sous-systeme a cree ses UserSettings. Tout enregistrement plus precoce
|
||||
// tombe dans le vide -- ULocalPlayer previent ses abonnes avant ses
|
||||
// sous-systemes. L'appel est idempotent, le repeter ne coute rien.
|
||||
if (USurvivalGameInstance* GameInstance = GetGameInstance<USurvivalGameInstance>())
|
||||
{
|
||||
GameInstance->RegisterMappableContexts();
|
||||
}
|
||||
|
||||
const UEnhancedInputUserSettings* Settings = GetInputSettings();
|
||||
const UEnhancedPlayerMappableKeyProfile* Profile = Settings ? Settings->GetActiveKeyProfile() : nullptr;
|
||||
|
||||
if (!Profile)
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("USettingsControlsWidget : aucun profil de touches. Verifie 'Enable User Settings' dans Project Settings > Enhanced Input."));
|
||||
return;
|
||||
}
|
||||
|
||||
// On parcourt les lignes DECLAREES et on va chercher leur mapping, plutot que
|
||||
// l'inverse : le profil contient aussi des mappings qu'on n'expose pas, et
|
||||
// l'ordre d'affichage est le notre, pas le sien.
|
||||
for (const TPair<FName, FKeyMappingRow>& Pair : Profile->GetPlayerMappingRows())
|
||||
{
|
||||
USettingKeyRowWidget* Row = FindRow(Pair.Key);
|
||||
if (!Row)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
FKey CurrentKey;
|
||||
for (const FPlayerKeyMapping& Mapping : Pair.Value.Mappings)
|
||||
{
|
||||
CurrentKey = Mapping.GetCurrentKey();
|
||||
break;
|
||||
}
|
||||
|
||||
Row->SetKey(CurrentKey);
|
||||
}
|
||||
}
|
||||
|
||||
void USettingsControlsWidget::ResetToDefaults()
|
||||
{
|
||||
UEnhancedInputUserSettings* Settings = GetInputSettings();
|
||||
if (!Settings)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (const USettingKeyRowWidget* Row : GetAllRows())
|
||||
{
|
||||
FMapPlayerKeyArgs Args;
|
||||
Args.MappingName = Row->GetMappingName();
|
||||
|
||||
FGameplayTagContainer FailureReason;
|
||||
Settings->ResetAllPlayerKeysInRow(Args, FailureReason);
|
||||
}
|
||||
|
||||
Settings->ApplySettings();
|
||||
RefreshFromSettings();
|
||||
}
|
||||
|
||||
void USettingsControlsWidget::HandleRebindRequested(FName MappingName)
|
||||
{
|
||||
if (!OwningMenu.IsValid())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
PendingMappingName = MappingName;
|
||||
PendingKey = FKey();
|
||||
ConflictingMappingName = NAME_None;
|
||||
|
||||
FOnSettingsKeyCaptureResult Callback;
|
||||
Callback.BindDynamic(this, &USettingsControlsWidget::HandleKeyCaptured);
|
||||
|
||||
OwningMenu->RequestKeyCapture(GetMappingDisplayName(MappingName), Callback);
|
||||
}
|
||||
|
||||
void USettingsControlsWidget::HandleKeyCaptured(FKey CapturedKey)
|
||||
{
|
||||
UEnhancedInputUserSettings* Settings = GetInputSettings();
|
||||
|
||||
// Touche invalide = le joueur a annule avec Echap.
|
||||
if (!CapturedKey.IsValid() || PendingMappingName.IsNone() || !Settings)
|
||||
{
|
||||
PendingMappingName = NAME_None;
|
||||
return;
|
||||
}
|
||||
|
||||
PendingKey = CapturedKey;
|
||||
|
||||
const UEnhancedPlayerMappableKeyProfile* Profile = Settings->GetActiveKeyProfile();
|
||||
if (!Profile)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Le moteur sait deja repondre a "qui utilise cette touche ?" : pas de
|
||||
// comparaison a la main sur toutes les lignes.
|
||||
TArray<FName> Owners;
|
||||
Profile->GetMappingNamesForKey(CapturedKey, Owners);
|
||||
|
||||
ConflictingMappingName = NAME_None;
|
||||
for (const FName& Owner : Owners)
|
||||
{
|
||||
// Se voler sa propre touche n'a rien d'un conflit.
|
||||
if (Owner != PendingMappingName && FindRow(Owner))
|
||||
{
|
||||
ConflictingMappingName = Owner;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (ConflictingMappingName.IsNone())
|
||||
{
|
||||
// Rien a arbitrer : on applique et on referme le voile.
|
||||
ApplyPendingRebind();
|
||||
|
||||
if (OwningMenu.IsValid())
|
||||
{
|
||||
OwningMenu->CloseKeyCapture();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!OwningMenu.IsValid())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
FOnSettingsConfirmationResult Callback;
|
||||
Callback.BindDynamic(this, &USettingsControlsWidget::HandleConflictAnswered);
|
||||
|
||||
const FText Message = FText::Format(ConflictMessage,
|
||||
PendingKey.GetDisplayName(),
|
||||
GetMappingDisplayName(ConflictingMappingName),
|
||||
GetMappingDisplayName(PendingMappingName));
|
||||
|
||||
// Le voile reste ouvert et change simplement d'etat : pour le joueur c'est
|
||||
// le meme geste qui continue, pas un second ecran qui apparait.
|
||||
OwningMenu->ShowKeyCaptureConflict(Message, Callback);
|
||||
}
|
||||
|
||||
void USettingsControlsWidget::HandleConflictAnswered(bool bConfirmed)
|
||||
{
|
||||
// Le voile s'est deja referme de lui-meme en repondant : il n'y a plus qu'a
|
||||
// appliquer, ou a oublier ce qu'on avait mis de cote.
|
||||
if (!bConfirmed)
|
||||
{
|
||||
PendingMappingName = NAME_None;
|
||||
ConflictingMappingName = NAME_None;
|
||||
PendingKey = FKey();
|
||||
return;
|
||||
}
|
||||
|
||||
ApplyPendingRebind();
|
||||
}
|
||||
|
||||
void USettingsControlsWidget::ApplyPendingRebind()
|
||||
{
|
||||
UEnhancedInputUserSettings* Settings = GetInputSettings();
|
||||
if (!Settings || PendingMappingName.IsNone() || !PendingKey.IsValid())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
FGameplayTagContainer FailureReason;
|
||||
|
||||
// L'ancien proprietaire perd sa touche AVANT qu'on assigne la notre : deux
|
||||
// actions sur la meme touche laisseraient un doublon actif, et le joueur
|
||||
// declencherait les deux d'un seul appui.
|
||||
//
|
||||
// On lui assigne une touche VIDE au lieu d'appeler UnMapPlayerKey : ce
|
||||
// dernier fait un ResetToDefault(), donc il rendrait a l'action sa touche
|
||||
// d'origine -- exactement celle qu'on est en train de lui prendre. Le nom
|
||||
// trompe, il annule la surcharge du joueur, il ne libere pas la touche.
|
||||
if (!ConflictingMappingName.IsNone())
|
||||
{
|
||||
FMapPlayerKeyArgs ClearArgs;
|
||||
ClearArgs.MappingName = ConflictingMappingName;
|
||||
ClearArgs.Slot = EPlayerMappableKeySlot::First;
|
||||
ClearArgs.NewKey = FKey();
|
||||
|
||||
Settings->MapPlayerKey(ClearArgs, FailureReason);
|
||||
}
|
||||
|
||||
FMapPlayerKeyArgs MapArgs;
|
||||
MapArgs.MappingName = PendingMappingName;
|
||||
MapArgs.Slot = EPlayerMappableKeySlot::First;
|
||||
MapArgs.NewKey = PendingKey;
|
||||
|
||||
Settings->MapPlayerKey(MapArgs, FailureReason);
|
||||
|
||||
if (!FailureReason.IsEmpty())
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("USettingsControlsWidget : remappage de '%s' refuse (%s)."),
|
||||
*PendingMappingName.ToString(), *FailureReason.ToString());
|
||||
}
|
||||
|
||||
// ApplySettings pousse les surcharges dans les mapping contexts deja
|
||||
// enregistres : le pawn n'a rien a rebrancher, la touche est active tout de
|
||||
// suite. La sauvegarde, elle, attend la fermeture de l'ecran.
|
||||
Settings->ApplySettings();
|
||||
|
||||
PendingMappingName = NAME_None;
|
||||
ConflictingMappingName = NAME_None;
|
||||
PendingKey = FKey();
|
||||
|
||||
// Les deux lignes concernees ont bouge : on relit tout plutot que de tenir
|
||||
// une comptabilite qui finirait par diverger.
|
||||
RefreshFromSettings();
|
||||
}
|
||||
|
||||
#undef LOCTEXT_NAMESPACE
|
||||
@@ -0,0 +1,506 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
|
||||
#include "SettingsDisplayWidget.h"
|
||||
|
||||
#include "Kismet/KismetSystemLibrary.h"
|
||||
#include "SettingRowWidget.h"
|
||||
#include "SettingsMenuWidget.h"
|
||||
#include "SurvivalUserSettings.h"
|
||||
|
||||
#define LOCTEXT_NAMESPACE "Reglages"
|
||||
|
||||
namespace SettingsDisplayTab
|
||||
{
|
||||
// Ordre des modes dans la ligne. Il doit correspondre a MakeWindowMode et a
|
||||
// FindDisplayModeIndex : trois listes a garder alignees, d'ou leur voisinage.
|
||||
constexpr int32 ModeFullscreen = 0;
|
||||
constexpr int32 ModeBorderless = 1;
|
||||
constexpr int32 ModeWindowed = 2;
|
||||
|
||||
EWindowMode::Type MakeWindowMode(int32 Index)
|
||||
{
|
||||
switch (Index)
|
||||
{
|
||||
case ModeBorderless: return EWindowMode::WindowedFullscreen;
|
||||
case ModeWindowed: return EWindowMode::Windowed;
|
||||
default: return EWindowMode::Fullscreen;
|
||||
}
|
||||
}
|
||||
|
||||
int32 FindDisplayModeIndex(EWindowMode::Type Mode)
|
||||
{
|
||||
switch (Mode)
|
||||
{
|
||||
case EWindowMode::WindowedFullscreen: return ModeBorderless;
|
||||
case EWindowMode::Windowed: return ModeWindowed;
|
||||
default: return ModeFullscreen;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Limites de FPS proposees. 0 = illimitee, c'est la convention de
|
||||
* SetFrameRateLimit et non une valeur sentinelle a nous.
|
||||
*/
|
||||
const TArray<float>& FrameRateLimits()
|
||||
{
|
||||
static const TArray<float> Limits = { 30.f, 60.f, 90.f, 120.f, 144.f, 165.f, 240.f, 0.f };
|
||||
return Limits;
|
||||
}
|
||||
|
||||
int32 FindFrameRateIndex(float Limit)
|
||||
{
|
||||
const TArray<float>& Limits = FrameRateLimits();
|
||||
|
||||
// Comparaison a la tolerance pres : la valeur relue du .ini a pu passer
|
||||
// par une conversion et ne plus etre strictement egale.
|
||||
for (int32 Index = 0; Index < Limits.Num(); ++Index)
|
||||
{
|
||||
if (FMath::IsNearlyEqual(Limits[Index], Limit, 0.5f))
|
||||
{
|
||||
return Index;
|
||||
}
|
||||
}
|
||||
|
||||
// Valeur exotique posee a la main dans le .ini : on retombe sur
|
||||
// "Illimitee" plutot que d'afficher n'importe quoi.
|
||||
return Limits.Num() - 1;
|
||||
}
|
||||
}
|
||||
|
||||
USettingsDisplayWidget::USettingsDisplayWidget(const FObjectInitializer& ObjectInitializer)
|
||||
: Super(ObjectInitializer)
|
||||
{
|
||||
// Dans le constructeur, pour la meme raison que ConflictMessage : ecrit dans
|
||||
// NativeConstruct, ce texte ecraserait a chaque ouverture celui regle dans le
|
||||
// Blueprint.
|
||||
VideoConfirmMessage = LOCTEXT("VideoConfirm",
|
||||
"Garder ce mode d'affichage ?\nRetour automatique dans {0} s.");
|
||||
}
|
||||
|
||||
void USettingsDisplayWidget::NativeConstruct()
|
||||
{
|
||||
Super::NativeConstruct();
|
||||
|
||||
using namespace SurvivalSettingsRange;
|
||||
|
||||
if (DisplayModeRow)
|
||||
{
|
||||
DisplayModeRow->SetupAsChoice(LOCTEXT("ModeAffichage", "Mode d'affichage"),
|
||||
{ LOCTEXT("PleinEcran", "Plein écran"),
|
||||
LOCTEXT("SansBordure", "Sans bordure"),
|
||||
LOCTEXT("Fenetre", "Fenêtré") }, 0);
|
||||
DisplayModeRow->OnIndexChanged.AddDynamic(this, &USettingsDisplayWidget::HandleDisplayModeChanged);
|
||||
}
|
||||
|
||||
if (ResolutionRow)
|
||||
{
|
||||
ResolutionRow->OnIndexChanged.AddDynamic(this, &USettingsDisplayWidget::HandleResolutionChanged);
|
||||
}
|
||||
|
||||
if (FrameRateLimitRow)
|
||||
{
|
||||
TArray<FText> Labels;
|
||||
Labels.Reserve(SettingsDisplayTab::FrameRateLimits().Num());
|
||||
for (const float Limit : SettingsDisplayTab::FrameRateLimits())
|
||||
{
|
||||
Labels.Add(FMath::IsNearlyZero(Limit)
|
||||
? LOCTEXT("Illimitee", "Illimitée")
|
||||
: FText::AsNumber(FMath::RoundToInt(Limit)));
|
||||
}
|
||||
|
||||
FrameRateLimitRow->SetupAsChoice(LOCTEXT("LimiteFPS", "Limite de FPS"), Labels, 0);
|
||||
FrameRateLimitRow->OnIndexChanged.AddDynamic(this, &USettingsDisplayWidget::HandleFrameRateLimitChanged);
|
||||
}
|
||||
|
||||
if (VSyncRow)
|
||||
{
|
||||
VSyncRow->SetupAsChoice(LOCTEXT("VSync", "Synchronisation verticale"),
|
||||
{ LOCTEXT("Non", "Non"), LOCTEXT("Oui", "Oui") }, 0);
|
||||
VSyncRow->OnIndexChanged.AddDynamic(this, &USettingsDisplayWidget::HandleVSyncChanged);
|
||||
}
|
||||
|
||||
if (ResolutionScaleRow)
|
||||
{
|
||||
ResolutionScaleRow->SetupAsNumber(LOCTEXT("EchelleResolution", "Échelle de résolution"),
|
||||
ResolutionScaleMin, ResolutionScaleMax, ResolutionScaleStep, ResolutionScaleArrowStep,
|
||||
ResolutionScaleMax, LOCTEXT("Pourcent", " %"));
|
||||
ResolutionScaleRow->OnIndexChanged.AddDynamic(this, &USettingsDisplayWidget::HandleResolutionScaleChanged);
|
||||
}
|
||||
|
||||
if (BrightnessRow)
|
||||
{
|
||||
BrightnessRow->SetupAsNumber(LOCTEXT("Luminosite", "Luminosité"),
|
||||
BrightnessMin, BrightnessMax, BrightnessStep, BrightnessArrowStep,
|
||||
BrightnessMin, LOCTEXT("Pourcent", " %"));
|
||||
BrightnessRow->OnIndexChanged.AddDynamic(this, &USettingsDisplayWidget::HandleBrightnessChanged);
|
||||
}
|
||||
|
||||
RebuildResolutionList();
|
||||
RefreshFromSettings();
|
||||
}
|
||||
|
||||
void USettingsDisplayWidget::RebuildResolutionList()
|
||||
{
|
||||
AvailableResolutions.Reset();
|
||||
|
||||
// La liste depend du mode : en fenetre on veut des tailles qui tiennent dans
|
||||
// l'ecran, pas les modes exclusifs de la carte graphique.
|
||||
const USurvivalUserSettings* Settings = USurvivalUserSettings::Get();
|
||||
const bool bWindowed = Settings && Settings->GetFullscreenMode() == EWindowMode::Windowed;
|
||||
|
||||
if (bWindowed)
|
||||
{
|
||||
UKismetSystemLibrary::GetConvenientWindowedResolutions(AvailableResolutions);
|
||||
}
|
||||
else
|
||||
{
|
||||
UKismetSystemLibrary::GetSupportedFullscreenResolutions(AvailableResolutions);
|
||||
}
|
||||
|
||||
if (AvailableResolutions.Num() == 0)
|
||||
{
|
||||
// Ecran non interroge (editeur, machine sans mode expose) : on propose au
|
||||
// moins la resolution courante pour ne pas afficher une ligne vide.
|
||||
UE_LOG(LogTemp, Warning, TEXT("USettingsDisplayWidget : aucune resolution retournee par le systeme, repli sur la resolution courante."));
|
||||
AvailableResolutions.Add(Settings ? Settings->GetScreenResolution() : FIntPoint(1920, 1080));
|
||||
}
|
||||
|
||||
FilterResolutions(Settings ? Settings->GetScreenResolution() : FIntPoint(1920, 1080));
|
||||
|
||||
if (!ResolutionRow)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
TArray<FText> Labels;
|
||||
Labels.Reserve(AvailableResolutions.Num());
|
||||
for (const FIntPoint& Resolution : AvailableResolutions)
|
||||
{
|
||||
// FString::Printf et non FText::AsNumber : ce dernier formate selon la
|
||||
// locale et insererait un separateur de milliers -- "1 920 x 1 080".
|
||||
// Une resolution s'ecrit "1920x1080", sans espace ni separateur : c'est
|
||||
// une reference technique, pas une quantite.
|
||||
Labels.Add(FText::FromString(FString::Printf(TEXT("%dx%d"), Resolution.X, Resolution.Y)));
|
||||
}
|
||||
|
||||
ResolutionRow->SetupAsChoice(LOCTEXT("Resolution", "Résolution"),
|
||||
Labels, FindCurrentResolutionIndex(Settings));
|
||||
}
|
||||
|
||||
void USettingsDisplayWidget::FilterResolutions(const FIntPoint& CurrentResolution)
|
||||
{
|
||||
// Des PLAGES et non un rapport exact : les resolutions commerciales ne
|
||||
// tombent pas juste. En 16:9, 1366x768 vaut 1.779 ; en "21:9", 2560x1080
|
||||
// vaut 2.370, 3440x1440 vaut 2.389 et 3840x1600 vaut 2.400 -- trois valeurs
|
||||
// differentes pour un format qu'on annonce pourtant comme un seul.
|
||||
constexpr float WideMin = 1.74f;
|
||||
constexpr float WideMax = 1.81f;
|
||||
constexpr float UltrawideMin = 2.30f;
|
||||
constexpr float UltrawideMax = 2.42f;
|
||||
|
||||
TArray<FIntPoint> Kept;
|
||||
Kept.Reserve(AvailableResolutions.Num());
|
||||
|
||||
for (const FIntPoint& Resolution : AvailableResolutions)
|
||||
{
|
||||
if (Resolution.X < MinimumResolution.X || Resolution.Y < MinimumResolution.Y)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const float Ratio = static_cast<float>(Resolution.X) / static_cast<float>(Resolution.Y);
|
||||
|
||||
const bool bIsWide = Ratio >= WideMin && Ratio <= WideMax;
|
||||
const bool bIsUltrawide = bAllowUltrawide && Ratio >= UltrawideMin && Ratio <= UltrawideMax;
|
||||
|
||||
// Tout le reste degage : 4:3, 5:4, 16:10, et le 32:9 des ecrans doubles,
|
||||
// qui deformerait le HUD sans qu'on ait rien prevu pour lui.
|
||||
if (!bIsWide && !bIsUltrawide)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Kept.Add(Resolution);
|
||||
}
|
||||
|
||||
// La resolution courante doit rester selectionnable meme si elle ne passe pas
|
||||
// le filtre : sinon un joueur en 16:10 verrait la ligne afficher autre chose
|
||||
// que ce qu'il utilise vraiment.
|
||||
if (!Kept.Contains(CurrentResolution) && CurrentResolution.X > 0 && CurrentResolution.Y > 0)
|
||||
{
|
||||
Kept.Add(CurrentResolution);
|
||||
}
|
||||
|
||||
// Un ecran qui ne proposerait aucun 16:9 vaut mieux non filtre que vide.
|
||||
if (Kept.IsEmpty())
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("USettingsDisplayWidget : aucune resolution ne passe le filtre, la liste complete est conservee."));
|
||||
return;
|
||||
}
|
||||
|
||||
// Tri croissant par surface : la plus petite en haut, la native en bas.
|
||||
Kept.Sort([](const FIntPoint& A, const FIntPoint& B)
|
||||
{
|
||||
return static_cast<int64>(A.X) * A.Y < static_cast<int64>(B.X) * B.Y;
|
||||
});
|
||||
|
||||
AvailableResolutions = MoveTemp(Kept);
|
||||
}
|
||||
|
||||
int32 USettingsDisplayWidget::FindCurrentResolutionIndex(const USurvivalUserSettings* Settings) const
|
||||
{
|
||||
if (!Settings)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
const FIntPoint Current = Settings->GetScreenResolution();
|
||||
const int32 Found = AvailableResolutions.IndexOfByKey(Current);
|
||||
|
||||
// Resolution absente de la liste (fenetre redimensionnee a la main) : on
|
||||
// retombe sur la plus grande, en fin de liste.
|
||||
return Found != INDEX_NONE ? Found : AvailableResolutions.Num() - 1;
|
||||
}
|
||||
|
||||
int32 USettingsDisplayWidget::FindLargestResolutionIndex() const
|
||||
{
|
||||
int32 BestIndex = INDEX_NONE;
|
||||
int64 BestArea = -1;
|
||||
|
||||
// On compare la SURFACE et non l'index : rien ne garantit que le systeme
|
||||
// renvoie ses modes triés, et un 2560x1080 ultralarge ne doit pas passer
|
||||
// devant un 1920x1200.
|
||||
for (int32 Index = 0; Index < AvailableResolutions.Num(); ++Index)
|
||||
{
|
||||
const int64 Area = static_cast<int64>(AvailableResolutions[Index].X) * AvailableResolutions[Index].Y;
|
||||
if (Area > BestArea)
|
||||
{
|
||||
BestArea = Area;
|
||||
BestIndex = Index;
|
||||
}
|
||||
}
|
||||
|
||||
return BestIndex;
|
||||
}
|
||||
|
||||
void USettingsDisplayWidget::RefreshFromSettings()
|
||||
{
|
||||
USurvivalUserSettings* Settings = USurvivalUserSettings::Get();
|
||||
if (!Settings)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (DisplayModeRow)
|
||||
{
|
||||
DisplayModeRow->SetIndexSilently(SettingsDisplayTab::FindDisplayModeIndex(Settings->GetFullscreenMode()));
|
||||
}
|
||||
|
||||
if (ResolutionRow)
|
||||
{
|
||||
ResolutionRow->SetIndexSilently(FindCurrentResolutionIndex(Settings));
|
||||
}
|
||||
|
||||
if (FrameRateLimitRow)
|
||||
{
|
||||
FrameRateLimitRow->SetIndexSilently(SettingsDisplayTab::FindFrameRateIndex(Settings->GetFrameRateLimit()));
|
||||
}
|
||||
|
||||
if (VSyncRow)
|
||||
{
|
||||
VSyncRow->SetIndexSilently(Settings->IsVSyncEnabled() ? 1 : 0);
|
||||
}
|
||||
|
||||
if (ResolutionScaleRow)
|
||||
{
|
||||
// On lit la valeur BRUTE en pourcentage et non la normalisee : la plage
|
||||
// autorisee par le moteur ne commence pas a 50 %, une valeur normalisee
|
||||
// ne correspondrait donc pas a ce que la ligne affiche.
|
||||
float CurrentNormalized = 0.f;
|
||||
float CurrentValue = 100.f;
|
||||
float MinValue = 0.f;
|
||||
float MaxValue = 100.f;
|
||||
Settings->GetResolutionScaleInformationEx(CurrentNormalized, CurrentValue, MinValue, MaxValue);
|
||||
|
||||
ResolutionScaleRow->SetNumericValueSilently(CurrentValue);
|
||||
}
|
||||
|
||||
if (BrightnessRow)
|
||||
{
|
||||
BrightnessRow->SetNumericValueSilently(Settings->GetBrightness());
|
||||
}
|
||||
}
|
||||
|
||||
void USettingsDisplayWidget::ResetToDefaults()
|
||||
{
|
||||
USurvivalUserSettings* Settings = USurvivalUserSettings::Get();
|
||||
if (!Settings)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// On ne touche PAS a la resolution ni au mode d'affichage : les remettre
|
||||
// "par defaut" pourrait basculer sur un mode que l'ecran ne gere pas, et le
|
||||
// joueur n'a pas demande ca en cliquant Reinitialiser.
|
||||
Settings->SetFrameRateLimit(0.f);
|
||||
Settings->SetVSyncEnabled(false);
|
||||
Settings->SetResolutionScaleNormalized(1.f);
|
||||
Settings->SetSurvivalDisplayToDefaults();
|
||||
|
||||
Settings->ApplyNonResolutionSettings();
|
||||
RefreshFromSettings();
|
||||
}
|
||||
|
||||
void USettingsDisplayWidget::HandleDisplayModeChanged(int32 NewIndex)
|
||||
{
|
||||
USurvivalUserSettings* Settings = USurvivalUserSettings::Get();
|
||||
if (!Settings)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Settings->SetFullscreenMode(SettingsDisplayTab::MakeWindowMode(NewIndex));
|
||||
|
||||
// Passer en fenetre change la liste des tailles proposables.
|
||||
RebuildResolutionList();
|
||||
|
||||
// En plein ecran ou sans bordure, on bascule d'office sur la plus grande
|
||||
// resolution disponible -- c'est-a-dire celle de l'ecran.
|
||||
//
|
||||
// Sans ca, un jeu lance en Standalone dans une fenetre de 1280x720 garderait
|
||||
// cette resolution en plein ecran : l'image serait etiree sur tout l'ecran et
|
||||
// visiblement floue, alors que le joueur vient de demander le contraire. En
|
||||
// fenetre on ne touche a rien, la taille choisie est celle qu'il veut.
|
||||
if (NewIndex != SettingsDisplayTab::ModeWindowed)
|
||||
{
|
||||
const int32 LargestIndex = FindLargestResolutionIndex();
|
||||
if (AvailableResolutions.IsValidIndex(LargestIndex))
|
||||
{
|
||||
Settings->SetScreenResolution(AvailableResolutions[LargestIndex]);
|
||||
|
||||
if (ResolutionRow)
|
||||
{
|
||||
ResolutionRow->SetIndexSilently(LargestIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ApplyVideoModeAndConfirm();
|
||||
}
|
||||
|
||||
void USettingsDisplayWidget::HandleResolutionChanged(int32 NewIndex)
|
||||
{
|
||||
USurvivalUserSettings* Settings = USurvivalUserSettings::Get();
|
||||
if (!Settings || !AvailableResolutions.IsValidIndex(NewIndex))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Settings->SetScreenResolution(AvailableResolutions[NewIndex]);
|
||||
ApplyVideoModeAndConfirm();
|
||||
}
|
||||
|
||||
void USettingsDisplayWidget::ApplyVideoModeAndConfirm()
|
||||
{
|
||||
USurvivalUserSettings* Settings = USurvivalUserSettings::Get();
|
||||
if (!Settings)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// ApplyResolutionSettings et non ApplySettings : on ne veut surtout pas
|
||||
// repousser la qualite graphique au passage, ce serait une saccade a chaque
|
||||
// clic de fleche.
|
||||
Settings->ApplyResolutionSettings(/*bCheckForCommandLineOverrides=*/false);
|
||||
|
||||
if (!OwningMenu.IsValid())
|
||||
{
|
||||
// Sans ecran parent, personne ne peut poser la question. On valide, au
|
||||
// prix du filet de securite -- mieux que de laisser un etat en suspens.
|
||||
Settings->ConfirmVideoMode();
|
||||
return;
|
||||
}
|
||||
|
||||
FOnSettingsConfirmationResult Callback;
|
||||
Callback.BindDynamic(this, &USettingsDisplayWidget::HandleVideoConfirmAnswered);
|
||||
|
||||
if (!OwningMenu->RequestConfirmation(VideoConfirmMessage, VideoConfirmSeconds, Callback))
|
||||
{
|
||||
Settings->ConfirmVideoMode();
|
||||
}
|
||||
}
|
||||
|
||||
void USettingsDisplayWidget::HandleVideoConfirmAnswered(bool bConfirmed)
|
||||
{
|
||||
USurvivalUserSettings* Settings = USurvivalUserSettings::Get();
|
||||
if (!Settings)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (bConfirmed)
|
||||
{
|
||||
// Ce mode devient la reference vers laquelle RevertVideoMode ramenera la
|
||||
// prochaine fois.
|
||||
Settings->ConfirmVideoMode();
|
||||
}
|
||||
else
|
||||
{
|
||||
Settings->RevertVideoMode();
|
||||
Settings->ApplyResolutionSettings(/*bCheckForCommandLineOverrides=*/false);
|
||||
RebuildResolutionList();
|
||||
}
|
||||
|
||||
RefreshFromSettings();
|
||||
}
|
||||
|
||||
void USettingsDisplayWidget::HandleFrameRateLimitChanged(int32 NewIndex)
|
||||
{
|
||||
USurvivalUserSettings* Settings = USurvivalUserSettings::Get();
|
||||
if (!Settings || !SettingsDisplayTab::FrameRateLimits().IsValidIndex(NewIndex))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Settings->SetFrameRateLimit(SettingsDisplayTab::FrameRateLimits()[NewIndex]);
|
||||
Settings->ApplyNonResolutionSettings();
|
||||
}
|
||||
|
||||
void USettingsDisplayWidget::HandleVSyncChanged(int32 NewIndex)
|
||||
{
|
||||
if (USurvivalUserSettings* Settings = USurvivalUserSettings::Get())
|
||||
{
|
||||
Settings->SetVSyncEnabled(NewIndex == 1);
|
||||
Settings->ApplyNonResolutionSettings();
|
||||
}
|
||||
}
|
||||
|
||||
void USettingsDisplayWidget::HandleResolutionScaleChanged(int32 NewIndex)
|
||||
{
|
||||
USurvivalUserSettings* Settings = USurvivalUserSettings::Get();
|
||||
if (!Settings || !ResolutionScaleRow)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Settings->SetResolutionScaleValueEx(ResolutionScaleRow->GetNumericValue());
|
||||
Settings->ApplyNonResolutionSettings();
|
||||
}
|
||||
|
||||
void USettingsDisplayWidget::HandleBrightnessChanged(int32 NewIndex)
|
||||
{
|
||||
USurvivalUserSettings* Settings = USurvivalUserSettings::Get();
|
||||
if (!Settings || !BrightnessRow)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Settings->SetBrightness(BrightnessRow->GetNumericValue());
|
||||
|
||||
// ApplySurvivalSettings et non ApplyNonResolutionSettings : la luminosite
|
||||
// est a nous, elle ne passe pas par le volet video du moteur.
|
||||
Settings->ApplySurvivalSettings();
|
||||
}
|
||||
|
||||
#undef LOCTEXT_NAMESPACE
|
||||
@@ -33,7 +33,7 @@ void USettingsGameplayWidget::NativeConstruct()
|
||||
if (SensitivityRow)
|
||||
{
|
||||
SensitivityRow->SetupAsNumber(LOCTEXT("Sensibilite", "Sensibilité"),
|
||||
SensitivityMin, SensitivityMax, SensitivityStep, SensitivityMin, FText::GetEmpty());
|
||||
SensitivityMin, SensitivityMax, SensitivityStep, SensitivityArrowStep, SensitivityMin, FText::GetEmpty());
|
||||
SensitivityRow->OnIndexChanged.AddDynamic(this, &USettingsGameplayWidget::HandleSensitivityChanged);
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ void USettingsGameplayWidget::NativeConstruct()
|
||||
if (SensitivityVerticalRow)
|
||||
{
|
||||
SensitivityVerticalRow->SetupAsNumber(LOCTEXT("SensibiliteVerticale", "Sensibilité verticale"),
|
||||
SensitivityMin, SensitivityMax, SensitivityStep, SensitivityMin, FText::GetEmpty());
|
||||
SensitivityMin, SensitivityMax, SensitivityStep, SensitivityArrowStep, SensitivityMin, FText::GetEmpty());
|
||||
SensitivityVerticalRow->OnIndexChanged.AddDynamic(this, &USettingsGameplayWidget::HandleSensitivityVerticalChanged);
|
||||
}
|
||||
|
||||
@@ -60,14 +60,14 @@ void USettingsGameplayWidget::NativeConstruct()
|
||||
if (FieldOfViewRow)
|
||||
{
|
||||
FieldOfViewRow->SetupAsNumber(LOCTEXT("ChampDeVision", "Champ de vision"),
|
||||
FieldOfViewMin, FieldOfViewMax, FieldOfViewStep, FieldOfViewMin, LOCTEXT("Degres", "°"));
|
||||
FieldOfViewMin, FieldOfViewMax, FieldOfViewStep, FieldOfViewArrowStep, 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", " %"));
|
||||
CameraShakeMin, CameraShakeMax, CameraShakeStep, CameraShakeArrowStep, CameraShakeMin, LOCTEXT("Pourcent", " %"));
|
||||
CameraShakeRow->OnIndexChanged.AddDynamic(this, &USettingsGameplayWidget::HandleCameraShakeChanged);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
|
||||
#include "SettingsGraphicsWidget.h"
|
||||
|
||||
#include "SettingRowWidget.h"
|
||||
#include "SurvivalUserSettings.h"
|
||||
|
||||
#define LOCTEXT_NAMESPACE "Reglages"
|
||||
|
||||
namespace SettingsGraphicsTab
|
||||
{
|
||||
/**
|
||||
* Index de "Personnalise" dans la ligne de qualite globale.
|
||||
*
|
||||
* Le moteur renvoie -1 quand les curseurs ne correspondent a aucun preset.
|
||||
* On l'affiche comme un cinquieme choix, mais SetMaxSelectableIndex(3)
|
||||
* empeche le joueur de le selectionner : c'est un etat dans lequel on tombe,
|
||||
* pas un reglage qu'on choisit.
|
||||
*/
|
||||
constexpr int32 CustomQualityIndex = 4;
|
||||
|
||||
/** Bas / Moyen / Eleve / Epique. Le moteur accepte aussi 4 (cinematique), qu'on n'expose pas. */
|
||||
constexpr int32 MaxQualityLevel = 3;
|
||||
}
|
||||
|
||||
void USettingsGraphicsWidget::SetupQualityRow(USettingRowWidget* Row, const FText& Label)
|
||||
{
|
||||
if (!Row)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Row->SetupAsChoice(Label,
|
||||
{ LOCTEXT("QualiteBas", "Bas"),
|
||||
LOCTEXT("QualiteMoyen", "Moyen"),
|
||||
LOCTEXT("QualiteEleve", "Élevé"),
|
||||
LOCTEXT("QualiteEpique", "Épique") }, 0);
|
||||
}
|
||||
|
||||
void USettingsGraphicsWidget::NativeConstruct()
|
||||
{
|
||||
Super::NativeConstruct();
|
||||
|
||||
if (OverallQualityRow)
|
||||
{
|
||||
OverallQualityRow->SetupAsChoice(LOCTEXT("QualiteGlobale", "Qualité globale"),
|
||||
{ LOCTEXT("QualiteBas", "Bas"),
|
||||
LOCTEXT("QualiteMoyen", "Moyen"),
|
||||
LOCTEXT("QualiteEleve", "Élevé"),
|
||||
LOCTEXT("QualiteEpique", "Épique"),
|
||||
LOCTEXT("QualitePerso", "Personnalisé") }, 0);
|
||||
|
||||
OverallQualityRow->SetMaxSelectableIndex(SettingsGraphicsTab::MaxQualityLevel);
|
||||
OverallQualityRow->OnIndexChanged.AddDynamic(this, &USettingsGraphicsWidget::HandleOverallQualityChanged);
|
||||
}
|
||||
|
||||
SetupQualityRow(ViewDistanceRow, LOCTEXT("DistanceAffichage", "Distance d'affichage"));
|
||||
SetupQualityRow(FoliageRow, LOCTEXT("Vegetation", "Végétation"));
|
||||
SetupQualityRow(ShadowRow, LOCTEXT("Ombres", "Ombres"));
|
||||
SetupQualityRow(TextureRow, LOCTEXT("Textures", "Textures"));
|
||||
SetupQualityRow(VisualEffectRow, LOCTEXT("Effets", "Effets"));
|
||||
SetupQualityRow(PostProcessRow, LOCTEXT("PostProcess", "Post-process"));
|
||||
SetupQualityRow(AntiAliasingRow, LOCTEXT("AntiAliasing", "Anti-aliasing"));
|
||||
SetupQualityRow(ReflectionRow, LOCTEXT("Reflexions", "Réflexions"));
|
||||
SetupQualityRow(GlobalIlluminationRow, LOCTEXT("EclairageGlobal", "Éclairage global"));
|
||||
SetupQualityRow(ShadingRow, LOCTEXT("Ombrage", "Ombrage"));
|
||||
|
||||
if (ViewDistanceRow) { ViewDistanceRow->OnIndexChanged.AddDynamic(this, &USettingsGraphicsWidget::HandleViewDistanceChanged); }
|
||||
if (FoliageRow) { FoliageRow->OnIndexChanged.AddDynamic(this, &USettingsGraphicsWidget::HandleFoliageChanged); }
|
||||
if (ShadowRow) { ShadowRow->OnIndexChanged.AddDynamic(this, &USettingsGraphicsWidget::HandleShadowChanged); }
|
||||
if (TextureRow) { TextureRow->OnIndexChanged.AddDynamic(this, &USettingsGraphicsWidget::HandleTextureChanged); }
|
||||
if (VisualEffectRow) { VisualEffectRow->OnIndexChanged.AddDynamic(this, &USettingsGraphicsWidget::HandleVisualEffectChanged); }
|
||||
if (PostProcessRow) { PostProcessRow->OnIndexChanged.AddDynamic(this, &USettingsGraphicsWidget::HandlePostProcessChanged); }
|
||||
if (AntiAliasingRow) { AntiAliasingRow->OnIndexChanged.AddDynamic(this, &USettingsGraphicsWidget::HandleAntiAliasingChanged); }
|
||||
if (ReflectionRow) { ReflectionRow->OnIndexChanged.AddDynamic(this, &USettingsGraphicsWidget::HandleReflectionChanged); }
|
||||
if (GlobalIlluminationRow) { GlobalIlluminationRow->OnIndexChanged.AddDynamic(this, &USettingsGraphicsWidget::HandleGlobalIlluminationChanged); }
|
||||
if (ShadingRow) { ShadingRow->OnIndexChanged.AddDynamic(this, &USettingsGraphicsWidget::HandleShadingChanged); }
|
||||
|
||||
RefreshFromSettings();
|
||||
}
|
||||
|
||||
void USettingsGraphicsWidget::RefreshFromSettings()
|
||||
{
|
||||
USurvivalUserSettings* Settings = USurvivalUserSettings::Get();
|
||||
if (!Settings)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (OverallQualityRow)
|
||||
{
|
||||
// -1 quand les curseurs ne collent a aucun preset : c'est la que
|
||||
// "Personnalise" s'affiche.
|
||||
const int32 Overall = Settings->GetOverallScalabilityLevel();
|
||||
OverallQualityRow->SetIndexSilently(Overall >= 0 ? Overall : SettingsGraphicsTab::CustomQualityIndex);
|
||||
}
|
||||
|
||||
if (ViewDistanceRow) { ViewDistanceRow->SetIndexSilently(Settings->GetViewDistanceQuality()); }
|
||||
if (FoliageRow) { FoliageRow->SetIndexSilently(Settings->GetFoliageQuality()); }
|
||||
if (ShadowRow) { ShadowRow->SetIndexSilently(Settings->GetShadowQuality()); }
|
||||
if (TextureRow) { TextureRow->SetIndexSilently(Settings->GetTextureQuality()); }
|
||||
if (VisualEffectRow) { VisualEffectRow->SetIndexSilently(Settings->GetVisualEffectQuality()); }
|
||||
if (PostProcessRow) { PostProcessRow->SetIndexSilently(Settings->GetPostProcessingQuality()); }
|
||||
if (AntiAliasingRow) { AntiAliasingRow->SetIndexSilently(Settings->GetAntiAliasingQuality()); }
|
||||
if (ReflectionRow) { ReflectionRow->SetIndexSilently(Settings->GetReflectionQuality()); }
|
||||
if (GlobalIlluminationRow) { GlobalIlluminationRow->SetIndexSilently(Settings->GetGlobalIlluminationQuality()); }
|
||||
if (ShadingRow) { ShadingRow->SetIndexSilently(Settings->GetShadingQuality()); }
|
||||
}
|
||||
|
||||
void USettingsGraphicsWidget::ResetToDefaults()
|
||||
{
|
||||
USurvivalUserSettings* Settings = USurvivalUserSettings::Get();
|
||||
if (!Settings)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// "Eleve" plutot que le benchmark automatique : sur une machine modeste,
|
||||
// RunHardwareBenchmark peut descendre tres bas et le joueur ne comprendrait
|
||||
// pas pourquoi "Reinitialiser" a degrade son jeu.
|
||||
Settings->SetOverallScalabilityLevel(2);
|
||||
Settings->ApplyNonResolutionSettings();
|
||||
|
||||
RefreshFromSettings();
|
||||
}
|
||||
|
||||
void USettingsGraphicsWidget::ApplyAndRefreshOverall()
|
||||
{
|
||||
USurvivalUserSettings* Settings = USurvivalUserSettings::Get();
|
||||
if (!Settings || !OverallQualityRow)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Settings->ApplyNonResolutionSettings();
|
||||
|
||||
// Toucher un curseur individuel fait presque toujours basculer la qualite
|
||||
// globale sur "Personnalise" : la ligne du haut doit suivre immediatement,
|
||||
// sinon elle affiche un preset qui ne correspond plus a rien.
|
||||
const int32 Overall = Settings->GetOverallScalabilityLevel();
|
||||
OverallQualityRow->SetIndexSilently(Overall >= 0 ? Overall : SettingsGraphicsTab::CustomQualityIndex);
|
||||
}
|
||||
|
||||
void USettingsGraphicsWidget::HandleOverallQualityChanged(int32 NewIndex)
|
||||
{
|
||||
USurvivalUserSettings* Settings = USurvivalUserSettings::Get();
|
||||
if (!Settings)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Settings->SetOverallScalabilityLevel(FMath::Clamp(NewIndex, 0, SettingsGraphicsTab::MaxQualityLevel));
|
||||
Settings->ApplyNonResolutionSettings();
|
||||
|
||||
// Un preset repositionne les dix autres lignes d'un coup.
|
||||
RefreshFromSettings();
|
||||
}
|
||||
|
||||
void USettingsGraphicsWidget::HandleViewDistanceChanged(int32 NewIndex)
|
||||
{
|
||||
if (USurvivalUserSettings* Settings = USurvivalUserSettings::Get())
|
||||
{
|
||||
Settings->SetViewDistanceQuality(NewIndex);
|
||||
ApplyAndRefreshOverall();
|
||||
}
|
||||
}
|
||||
|
||||
void USettingsGraphicsWidget::HandleFoliageChanged(int32 NewIndex)
|
||||
{
|
||||
if (USurvivalUserSettings* Settings = USurvivalUserSettings::Get())
|
||||
{
|
||||
Settings->SetFoliageQuality(NewIndex);
|
||||
ApplyAndRefreshOverall();
|
||||
}
|
||||
}
|
||||
|
||||
void USettingsGraphicsWidget::HandleShadowChanged(int32 NewIndex)
|
||||
{
|
||||
if (USurvivalUserSettings* Settings = USurvivalUserSettings::Get())
|
||||
{
|
||||
Settings->SetShadowQuality(NewIndex);
|
||||
ApplyAndRefreshOverall();
|
||||
}
|
||||
}
|
||||
|
||||
void USettingsGraphicsWidget::HandleTextureChanged(int32 NewIndex)
|
||||
{
|
||||
if (USurvivalUserSettings* Settings = USurvivalUserSettings::Get())
|
||||
{
|
||||
Settings->SetTextureQuality(NewIndex);
|
||||
ApplyAndRefreshOverall();
|
||||
}
|
||||
}
|
||||
|
||||
void USettingsGraphicsWidget::HandleVisualEffectChanged(int32 NewIndex)
|
||||
{
|
||||
if (USurvivalUserSettings* Settings = USurvivalUserSettings::Get())
|
||||
{
|
||||
Settings->SetVisualEffectQuality(NewIndex);
|
||||
ApplyAndRefreshOverall();
|
||||
}
|
||||
}
|
||||
|
||||
void USettingsGraphicsWidget::HandlePostProcessChanged(int32 NewIndex)
|
||||
{
|
||||
if (USurvivalUserSettings* Settings = USurvivalUserSettings::Get())
|
||||
{
|
||||
Settings->SetPostProcessingQuality(NewIndex);
|
||||
ApplyAndRefreshOverall();
|
||||
}
|
||||
}
|
||||
|
||||
void USettingsGraphicsWidget::HandleAntiAliasingChanged(int32 NewIndex)
|
||||
{
|
||||
if (USurvivalUserSettings* Settings = USurvivalUserSettings::Get())
|
||||
{
|
||||
Settings->SetAntiAliasingQuality(NewIndex);
|
||||
ApplyAndRefreshOverall();
|
||||
}
|
||||
}
|
||||
|
||||
void USettingsGraphicsWidget::HandleReflectionChanged(int32 NewIndex)
|
||||
{
|
||||
if (USurvivalUserSettings* Settings = USurvivalUserSettings::Get())
|
||||
{
|
||||
Settings->SetReflectionQuality(NewIndex);
|
||||
ApplyAndRefreshOverall();
|
||||
}
|
||||
}
|
||||
|
||||
void USettingsGraphicsWidget::HandleGlobalIlluminationChanged(int32 NewIndex)
|
||||
{
|
||||
if (USurvivalUserSettings* Settings = USurvivalUserSettings::Get())
|
||||
{
|
||||
Settings->SetGlobalIlluminationQuality(NewIndex);
|
||||
ApplyAndRefreshOverall();
|
||||
}
|
||||
}
|
||||
|
||||
void USettingsGraphicsWidget::HandleShadingChanged(int32 NewIndex)
|
||||
{
|
||||
if (USurvivalUserSettings* Settings = USurvivalUserSettings::Get())
|
||||
{
|
||||
Settings->SetShadingQuality(NewIndex);
|
||||
ApplyAndRefreshOverall();
|
||||
}
|
||||
}
|
||||
|
||||
#undef LOCTEXT_NAMESPACE
|
||||
@@ -6,7 +6,11 @@
|
||||
#include "Components/Button.h"
|
||||
#include "Components/WidgetSwitcher.h"
|
||||
#include "ConfirmDialogWidget.h"
|
||||
#include "EnhancedInputSubsystems.h"
|
||||
#include "Engine/LocalPlayer.h"
|
||||
#include "InputCoreTypes.h"
|
||||
#include "KeyCaptureWidget.h"
|
||||
#include "UserSettings/EnhancedInputUserSettings.h"
|
||||
#include "SettingsTabWidget.h"
|
||||
#include "SurvivalUserSettings.h"
|
||||
|
||||
@@ -19,7 +23,8 @@ namespace
|
||||
constexpr int32 TabIndexGameplay = 0;
|
||||
constexpr int32 TabIndexDisplay = 1;
|
||||
constexpr int32 TabIndexGraphics = 2;
|
||||
constexpr int32 TabIndexAudio = 3;
|
||||
constexpr int32 TabIndexControls = 3;
|
||||
constexpr int32 TabIndexAudio = 4;
|
||||
}
|
||||
|
||||
USettingsMenuWidget::USettingsMenuWidget(const FObjectInitializer& ObjectInitializer)
|
||||
@@ -62,16 +67,27 @@ void USettingsMenuWidget::NativeConstruct()
|
||||
GraphicsTabButton->OnClicked.AddDynamic(this, &USettingsMenuWidget::HandleGraphicsTabClicked);
|
||||
}
|
||||
|
||||
if (ControlsTabButton)
|
||||
{
|
||||
ControlsTabButton->OnClicked.AddDynamic(this, &USettingsMenuWidget::HandleControlsTabClicked);
|
||||
}
|
||||
|
||||
if (AudioTabButton)
|
||||
{
|
||||
AudioTabButton->OnClicked.AddDynamic(this, &USettingsMenuWidget::HandleAudioTabClicked);
|
||||
}
|
||||
|
||||
if (ResetDialog)
|
||||
// Chaque onglet recoit un pointeur vers nous : c'est par la qu'il demandera
|
||||
// une confirmation, sans avoir a fabriquer sa propre boite.
|
||||
if (TabSwitcher)
|
||||
{
|
||||
ResetDialog->OnConfirmed.AddDynamic(this, &USettingsMenuWidget::HandleResetConfirmed);
|
||||
ResetDialog->OnCancelled.AddDynamic(this, &USettingsMenuWidget::HandleResetCancelled);
|
||||
ResetDialog->Close();
|
||||
for (int32 Index = 0; Index < TabSwitcher->GetNumWidgets(); ++Index)
|
||||
{
|
||||
if (USettingsTabWidget* Tab = Cast<USettingsTabWidget>(TabSwitcher->GetWidgetAtIndex(Index)))
|
||||
{
|
||||
Tab->SetOwningMenu(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
UpdateTabButtonStates();
|
||||
@@ -89,6 +105,20 @@ void USettingsMenuWidget::NativeDestruct()
|
||||
ResetButton->OnClicked.RemoveDynamic(this, &USettingsMenuWidget::HandleResetClicked);
|
||||
}
|
||||
|
||||
// La boite et le voile vivent au viewport, pas dans notre arbre : ils ne
|
||||
// disparaitraient pas avec nous.
|
||||
if (ResetDialog)
|
||||
{
|
||||
ResetDialog->RemoveFromParent();
|
||||
ResetDialog = nullptr;
|
||||
}
|
||||
|
||||
if (KeyCapture)
|
||||
{
|
||||
KeyCapture->RemoveFromParent();
|
||||
KeyCapture = nullptr;
|
||||
}
|
||||
|
||||
Super::NativeDestruct();
|
||||
}
|
||||
|
||||
@@ -96,6 +126,9 @@ void USettingsMenuWidget::Open()
|
||||
{
|
||||
SetVisibility(ESlateVisibility::Visible);
|
||||
|
||||
// L'ecran a pu etre referme sur un autre onglet que celui d'ouverture.
|
||||
UpdateTabButtonStates();
|
||||
|
||||
// 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())
|
||||
@@ -109,12 +142,18 @@ void USettingsMenuWidget::Open()
|
||||
|
||||
void USettingsMenuWidget::Close()
|
||||
{
|
||||
if (ResetDialog)
|
||||
// Fermer l'ecran avec une question en suspens vaut un refus : sinon le
|
||||
// demandeur -- typiquement un changement de resolution en attente de
|
||||
// confirmation -- resterait dans un etat non tranche.
|
||||
if (bResetDialogOpen)
|
||||
{
|
||||
ResetDialog->Close();
|
||||
FinishConfirmation(false);
|
||||
}
|
||||
|
||||
bResetDialogOpen = false;
|
||||
if (bKeyCaptureOpen)
|
||||
{
|
||||
CloseKeyCapture();
|
||||
}
|
||||
|
||||
SetVisibility(ESlateVisibility::Collapsed);
|
||||
|
||||
@@ -124,6 +163,20 @@ void USettingsMenuWidget::Close()
|
||||
{
|
||||
Settings->SaveSettings();
|
||||
}
|
||||
|
||||
// Les touches remappees vivent ailleurs : Enhanced Input les serialise pour
|
||||
// son compte, dans son propre fichier. Deux systemes de persistance, mais un
|
||||
// seul moment ou l'on ecrit.
|
||||
if (const ULocalPlayer* LocalPlayer = GetOwningLocalPlayer())
|
||||
{
|
||||
if (UEnhancedInputLocalPlayerSubsystem* Subsystem = LocalPlayer->GetSubsystem<UEnhancedInputLocalPlayerSubsystem>())
|
||||
{
|
||||
if (UEnhancedInputUserSettings* InputSettings = Subsystem->GetUserSettings())
|
||||
{
|
||||
InputSettings->SaveSettings();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FReply USettingsMenuWidget::NativeOnKeyDown(const FGeometry& InGeometry, const FKeyEvent& InKeyEvent)
|
||||
@@ -132,12 +185,18 @@ FReply USettingsMenuWidget::NativeOnKeyDown(const FGeometry& InGeometry, const F
|
||||
|
||||
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.
|
||||
// La boite et le voile gerent Echap eux-memes quand ils ont le focus.
|
||||
// S'ils l'ont perdu, on annule ici plutot que de fermer tout l'ecran en
|
||||
// laissant la question ou le voile affiche.
|
||||
if (bKeyCaptureOpen)
|
||||
{
|
||||
CloseKeyCapture();
|
||||
return FReply::Handled();
|
||||
}
|
||||
|
||||
if (bResetDialogOpen)
|
||||
{
|
||||
HandleResetCancelled();
|
||||
FinishConfirmation(false);
|
||||
return FReply::Handled();
|
||||
}
|
||||
|
||||
@@ -165,6 +224,9 @@ void USettingsMenuWidget::ShowTab(int32 Index)
|
||||
|
||||
TabSwitcher->SetActiveWidgetIndex(Index);
|
||||
|
||||
// L'onglet a change, donc le bouton a griser aussi.
|
||||
UpdateTabButtonStates();
|
||||
|
||||
if (USettingsTabWidget* ActiveTab = GetActiveTab())
|
||||
{
|
||||
ActiveTab->RefreshFromSettings();
|
||||
@@ -174,26 +236,24 @@ void USettingsMenuWidget::ShowTab(int32 Index)
|
||||
void USettingsMenuWidget::UpdateTabButtonStates()
|
||||
{
|
||||
const int32 PageCount = TabSwitcher ? TabSwitcher->GetNumWidgets() : 0;
|
||||
const int32 ActiveIndex = TabSwitcher ? TabSwitcher->GetActiveWidgetIndex() : INDEX_NONE;
|
||||
|
||||
if (GameplayTabButton)
|
||||
// Un bouton est cliquable si sa page existe ET qu'on n'y est pas deja.
|
||||
// Griser l'onglet courant lui sert d'indicateur de position : sans ca, rien
|
||||
// ne distingue visuellement l'onglet affiche des autres.
|
||||
auto ApplyState = [PageCount, ActiveIndex](UButton* Button, int32 TabIndex)
|
||||
{
|
||||
GameplayTabButton->SetIsEnabled(PageCount > TabIndexGameplay);
|
||||
}
|
||||
if (Button)
|
||||
{
|
||||
Button->SetIsEnabled(PageCount > TabIndex && ActiveIndex != TabIndex);
|
||||
}
|
||||
};
|
||||
|
||||
if (DisplayTabButton)
|
||||
{
|
||||
DisplayTabButton->SetIsEnabled(PageCount > TabIndexDisplay);
|
||||
}
|
||||
|
||||
if (GraphicsTabButton)
|
||||
{
|
||||
GraphicsTabButton->SetIsEnabled(PageCount > TabIndexGraphics);
|
||||
}
|
||||
|
||||
if (AudioTabButton)
|
||||
{
|
||||
AudioTabButton->SetIsEnabled(PageCount > TabIndexAudio);
|
||||
}
|
||||
ApplyState(GameplayTabButton, TabIndexGameplay);
|
||||
ApplyState(DisplayTabButton, TabIndexDisplay);
|
||||
ApplyState(GraphicsTabButton, TabIndexGraphics);
|
||||
ApplyState(ControlsTabButton, TabIndexControls);
|
||||
ApplyState(AudioTabButton, TabIndexAudio);
|
||||
}
|
||||
|
||||
void USettingsMenuWidget::HandleBackClicked()
|
||||
@@ -201,20 +261,66 @@ void USettingsMenuWidget::HandleBackClicked()
|
||||
OnCloseRequested.Broadcast();
|
||||
}
|
||||
|
||||
void USettingsMenuWidget::HandleResetClicked()
|
||||
UConfirmDialogWidget* USettingsMenuWidget::EnsureDialog()
|
||||
{
|
||||
// Sans boite assignee on ne bloque pas le joueur : on reinitialise.
|
||||
if (!ResetDialog)
|
||||
if (ResetDialog)
|
||||
{
|
||||
HandleResetConfirmed();
|
||||
return;
|
||||
return ResetDialog;
|
||||
}
|
||||
|
||||
bResetDialogOpen = true;
|
||||
ResetDialog->Open(ResetDialogMessage);
|
||||
if (!ResetDialogClass)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
ResetDialog = CreateWidget<UConfirmDialogWidget>(GetOwningPlayer(), ResetDialogClass);
|
||||
if (!ResetDialog)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
ResetDialog->OnConfirmed.AddDynamic(this, &USettingsMenuWidget::HandleDialogConfirmed);
|
||||
ResetDialog->OnCancelled.AddDynamic(this, &USettingsMenuWidget::HandleDialogCancelled);
|
||||
ResetDialog->AddToViewport(DialogZOrder);
|
||||
|
||||
return ResetDialog;
|
||||
}
|
||||
|
||||
void USettingsMenuWidget::HandleResetConfirmed()
|
||||
bool USettingsMenuWidget::RequestConfirmation(const FText& MessageOrFormat, float CountdownSeconds, const FOnSettingsConfirmationResult& OnResult)
|
||||
{
|
||||
UConfirmDialogWidget* Dialog = EnsureDialog();
|
||||
if (!Dialog)
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("USettingsMenuWidget : ResetDialogClass n'est pas assignee, la question ne peut pas etre posee."));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Une seule boite pour tout l'ecran : si une question etait deja posee, la
|
||||
// nouvelle l'ecrase. On repond "non" a l'ancienne pour que son demandeur ne
|
||||
// reste pas suspendu a une reponse qui n'arrivera plus.
|
||||
if (PendingConfirmation.IsBound())
|
||||
{
|
||||
const FOnSettingsConfirmationResult Superseded = PendingConfirmation;
|
||||
PendingConfirmation.Unbind();
|
||||
Superseded.ExecuteIfBound(false);
|
||||
}
|
||||
|
||||
PendingConfirmation = OnResult;
|
||||
bResetDialogOpen = true;
|
||||
|
||||
if (CountdownSeconds > 0.f)
|
||||
{
|
||||
Dialog->OpenWithCountdown(MessageOrFormat, CountdownSeconds);
|
||||
}
|
||||
else
|
||||
{
|
||||
Dialog->Open(MessageOrFormat);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void USettingsMenuWidget::FinishConfirmation(bool bConfirmed)
|
||||
{
|
||||
bResetDialogOpen = false;
|
||||
|
||||
@@ -223,26 +329,51 @@ void USettingsMenuWidget::HandleResetConfirmed()
|
||||
ResetDialog->Close();
|
||||
}
|
||||
|
||||
// Detache AVANT d'appeler : le demandeur a parfaitement le droit de reposer
|
||||
// une question dans son propre handler.
|
||||
const FOnSettingsConfirmationResult Answered = PendingConfirmation;
|
||||
PendingConfirmation.Unbind();
|
||||
|
||||
SetKeyboardFocus();
|
||||
|
||||
Answered.ExecuteIfBound(bConfirmed);
|
||||
}
|
||||
|
||||
void USettingsMenuWidget::HandleDialogConfirmed()
|
||||
{
|
||||
FinishConfirmation(true);
|
||||
}
|
||||
|
||||
void USettingsMenuWidget::HandleDialogCancelled()
|
||||
{
|
||||
FinishConfirmation(false);
|
||||
}
|
||||
|
||||
void USettingsMenuWidget::HandleResetClicked()
|
||||
{
|
||||
FOnSettingsConfirmationResult Callback;
|
||||
Callback.BindDynamic(this, &USettingsMenuWidget::HandleResetAnswered);
|
||||
|
||||
// Sans boite configuree on ne bloque pas le joueur : on reinitialise.
|
||||
if (!RequestConfirmation(ResetDialogMessage, 0.f, Callback))
|
||||
{
|
||||
HandleResetAnswered(true);
|
||||
}
|
||||
}
|
||||
|
||||
void USettingsMenuWidget::HandleResetAnswered(bool bConfirmed)
|
||||
{
|
||||
if (!bConfirmed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 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()
|
||||
@@ -260,9 +391,127 @@ void USettingsMenuWidget::HandleGraphicsTabClicked()
|
||||
ShowTab(TabIndexGraphics);
|
||||
}
|
||||
|
||||
void USettingsMenuWidget::HandleControlsTabClicked()
|
||||
{
|
||||
ShowTab(TabIndexControls);
|
||||
}
|
||||
|
||||
void USettingsMenuWidget::HandleAudioTabClicked()
|
||||
{
|
||||
ShowTab(TabIndexAudio);
|
||||
}
|
||||
|
||||
bool USettingsMenuWidget::RequestKeyCapture(const FText& ActionLabel, const FOnSettingsKeyCaptureResult& OnResult)
|
||||
{
|
||||
if (!KeyCaptureClass)
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("USettingsMenuWidget : KeyCaptureClass n'est pas assignee, aucun remappage possible."));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Cree au premier remappage seulement, puis reutilise -- comme la boite.
|
||||
if (!KeyCapture)
|
||||
{
|
||||
KeyCapture = CreateWidget<UKeyCaptureWidget>(GetOwningPlayer(), KeyCaptureClass);
|
||||
if (!KeyCapture)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
KeyCapture->OnKeyCaptured.AddDynamic(this, &USettingsMenuWidget::HandleKeyCaptured);
|
||||
KeyCapture->OnCaptureCancelled.AddDynamic(this, &USettingsMenuWidget::HandleKeyCaptureCancelled);
|
||||
KeyCapture->OnConflictAnswered.AddDynamic(this, &USettingsMenuWidget::HandleKeyConflictAnswered);
|
||||
|
||||
// Au-dessus de la boite : une confirmation de conflit s'ouvre APRES la
|
||||
// capture, mais le voile doit couvrir l'ecran pendant qu'il attend.
|
||||
KeyCapture->AddToViewport(DialogZOrder + 1);
|
||||
}
|
||||
|
||||
// Une capture deja en cours est abandonnee : son demandeur recoit une touche
|
||||
// invalide plutot que de rester suspendu.
|
||||
if (PendingKeyCapture.IsBound())
|
||||
{
|
||||
const FOnSettingsKeyCaptureResult Superseded = PendingKeyCapture;
|
||||
PendingKeyCapture.Unbind();
|
||||
Superseded.ExecuteIfBound(FKey());
|
||||
}
|
||||
|
||||
PendingKeyCapture = OnResult;
|
||||
bKeyCaptureOpen = true;
|
||||
KeyCapture->BeginCapture(ActionLabel);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void USettingsMenuWidget::ShowKeyCaptureConflict(const FText& Message, const FOnSettingsConfirmationResult& OnAnswer)
|
||||
{
|
||||
if (!KeyCapture || !bKeyCaptureOpen)
|
||||
{
|
||||
// Le voile a ete referme entre-temps : on repond "non" plutot que de
|
||||
// laisser le demandeur suspendu.
|
||||
OnAnswer.ExecuteIfBound(false);
|
||||
return;
|
||||
}
|
||||
|
||||
PendingConflictAnswer = OnAnswer;
|
||||
KeyCapture->ShowConflict(Message);
|
||||
}
|
||||
|
||||
void USettingsMenuWidget::CloseKeyCapture()
|
||||
{
|
||||
bKeyCaptureOpen = false;
|
||||
|
||||
if (KeyCapture)
|
||||
{
|
||||
KeyCapture->EndCapture();
|
||||
}
|
||||
|
||||
// On detache tout : si quelqu'un attendait encore une reponse, il la recoit
|
||||
// negative plutot que jamais.
|
||||
const FOnSettingsKeyCaptureResult AbandonedCapture = PendingKeyCapture;
|
||||
const FOnSettingsConfirmationResult AbandonedConflict = PendingConflictAnswer;
|
||||
PendingKeyCapture.Unbind();
|
||||
PendingConflictAnswer.Unbind();
|
||||
|
||||
SetKeyboardFocus();
|
||||
|
||||
AbandonedCapture.ExecuteIfBound(FKey());
|
||||
AbandonedConflict.ExecuteIfBound(false);
|
||||
}
|
||||
|
||||
void USettingsMenuWidget::HandleKeyCaptured(FKey CapturedKey)
|
||||
{
|
||||
// Le voile RESTE ouvert : c'est l'onglet qui decide s'il y a conflit, et
|
||||
// donc s'il faut le basculer sur la question ou le refermer.
|
||||
const FOnSettingsKeyCaptureResult Answered = PendingKeyCapture;
|
||||
PendingKeyCapture.Unbind();
|
||||
|
||||
Answered.ExecuteIfBound(CapturedKey);
|
||||
}
|
||||
|
||||
void USettingsMenuWidget::HandleKeyCaptureCancelled()
|
||||
{
|
||||
// Echap pendant la capture : rien a arbitrer, on referme.
|
||||
CloseKeyCapture();
|
||||
}
|
||||
|
||||
void USettingsMenuWidget::HandleKeyConflictAnswered(bool bConfirmed)
|
||||
{
|
||||
// Detache AVANT d'appeler : le demandeur applique le remappage dans son
|
||||
// handler et pourrait vouloir rouvrir une capture derriere.
|
||||
const FOnSettingsConfirmationResult Answered = PendingConflictAnswer;
|
||||
PendingConflictAnswer.Unbind();
|
||||
|
||||
bKeyCaptureOpen = false;
|
||||
|
||||
if (KeyCapture)
|
||||
{
|
||||
KeyCapture->EndCapture();
|
||||
}
|
||||
|
||||
SetKeyboardFocus();
|
||||
|
||||
Answered.ExecuteIfBound(bConfirmed);
|
||||
}
|
||||
|
||||
#undef LOCTEXT_NAMESPACE
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
|
||||
#include "SettingsTabWidget.h"
|
||||
|
||||
#include "SettingsMenuWidget.h"
|
||||
|
||||
void USettingsTabWidget::SetOwningMenu(USettingsMenuWidget* InMenu)
|
||||
{
|
||||
OwningMenu = InMenu;
|
||||
}
|
||||
@@ -3,12 +3,20 @@
|
||||
|
||||
#include "SurvivalGameInstance.h"
|
||||
|
||||
#include "EnhancedInputSubsystems.h"
|
||||
#include "Engine/LocalPlayer.h"
|
||||
#include "InputMappingContext.h"
|
||||
#include "SurvivalUserSettings.h"
|
||||
#include "UserSettings/EnhancedInputUserSettings.h"
|
||||
|
||||
void USurvivalGameInstance::Init()
|
||||
{
|
||||
Super::Init();
|
||||
|
||||
// Pas d'enregistrement des IMC ici : a ce stade il n'y a ni joueur local ni
|
||||
// PlayerController, donc pas encore de UserSettings a qui parler.
|
||||
// RegisterMappableContexts() est appelee par l'ecran de reglages.
|
||||
|
||||
// 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
|
||||
@@ -25,3 +33,38 @@ void USurvivalGameInstance::Init()
|
||||
UE_LOG(LogTemp, Error, TEXT("USurvivalGameInstance : GameUserSettingsClassName n'est pas configure dans DefaultEngine.ini, aucun reglage ne sera pris en compte."));
|
||||
}
|
||||
}
|
||||
|
||||
void USurvivalGameInstance::RegisterMappableContexts()
|
||||
{
|
||||
ULocalPlayer* LocalPlayer = GetFirstGamePlayer();
|
||||
if (!LocalPlayer)
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("USurvivalGameInstance : aucun joueur local, enregistrement des touches remis a plus tard."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (MappableContexts.IsEmpty())
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("USurvivalGameInstance : MappableContexts est vide, l'onglet Touches affichera 'Non assignee' partout. Ajoute IMC_Default dans le Blueprint de GameInstance."));
|
||||
return;
|
||||
}
|
||||
|
||||
UEnhancedInputLocalPlayerSubsystem* Subsystem = LocalPlayer->GetSubsystem<UEnhancedInputLocalPlayerSubsystem>();
|
||||
UEnhancedInputUserSettings* InputSettings = Subsystem ? Subsystem->GetUserSettings() : nullptr;
|
||||
|
||||
if (!InputSettings)
|
||||
{
|
||||
UE_LOG(LogTemp, Error, TEXT("USurvivalGameInstance : reglages d'input introuvables. Coche 'Enable User Settings' dans Project Settings > Enhanced Input."));
|
||||
return;
|
||||
}
|
||||
|
||||
for (const TObjectPtr<UInputMappingContext>& Context : MappableContexts)
|
||||
{
|
||||
if (Context)
|
||||
{
|
||||
// Idempotent : le moteur ignore un contexte deja connu, on peut donc
|
||||
// rappeler cette fonction sans precaution.
|
||||
InputSettings->RegisterInputMappingContext(Context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ USurvivalUserSettings::USurvivalUserSettings()
|
||||
// valeurs sont donc bien les defauts d'usine, ecrasees ensuite par le
|
||||
// fichier s'il existe.
|
||||
SetSurvivalGameplayToDefaults();
|
||||
SetSurvivalDisplayToDefaults();
|
||||
}
|
||||
|
||||
USurvivalUserSettings* USurvivalUserSettings::Get()
|
||||
@@ -40,17 +41,47 @@ void USurvivalUserSettings::SetSurvivalGameplayToDefaults()
|
||||
bCrouchToggle = false;
|
||||
}
|
||||
|
||||
void USurvivalUserSettings::SetSurvivalDisplayToDefaults()
|
||||
{
|
||||
Brightness = 100.f;
|
||||
}
|
||||
|
||||
void USurvivalUserSettings::SetToDefaults()
|
||||
{
|
||||
// Le "Reinitialiser" global du moteur doit aussi remettre NOS valeurs :
|
||||
// sans cette surcharge, l'onglet Jeu resterait seul inchange.
|
||||
// sans cette surcharge, elles resteraient seules inchangees.
|
||||
Super::SetToDefaults();
|
||||
|
||||
SetSurvivalGameplayToDefaults();
|
||||
SetSurvivalDisplayToDefaults();
|
||||
}
|
||||
|
||||
void USurvivalUserSettings::SetBrightness(float Value)
|
||||
{
|
||||
using namespace SurvivalSettingsRange;
|
||||
Brightness = SnapToStep(Value, BrightnessMin, BrightnessMax, BrightnessStep);
|
||||
}
|
||||
|
||||
void USurvivalUserSettings::ApplySurvivalSettings()
|
||||
{
|
||||
// Le gamma n'appartient pas a UGameUserSettings : il vit sur GEngine, donc
|
||||
// sur un objet GLOBAL au processus. Deux consequences :
|
||||
//
|
||||
// 1. DisplayGamma est un EXPOSANT dont la valeur moteur est 2.2, pas un
|
||||
// multiplicateur neutre a 1. On module la reference, on ne la remplace
|
||||
// pas.
|
||||
// 2. En editeur, cet objet est celui de l'editeur : y toucher assombrit
|
||||
// toute l'interface, y compris apres avoir quitte le PIE. On ne l'applique
|
||||
// donc qu'en jeu. La luminosite ne se teste pas en PIE, mais en
|
||||
// Standalone -- qui est un processus separe.
|
||||
if (GEngine && !GIsEditor)
|
||||
{
|
||||
using namespace SurvivalSettingsRange;
|
||||
|
||||
const float Normalized = FMath::Clamp(Brightness, BrightnessMin, BrightnessMax) / 100.f;
|
||||
GEngine->DisplayGamma = BaseDisplayGamma * Normalized;
|
||||
}
|
||||
|
||||
OnSurvivalSettingsApplied.Broadcast();
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Blueprint/UserWidget.h"
|
||||
#include "Containers/Ticker.h"
|
||||
#include "ConfirmDialogWidget.generated.h"
|
||||
|
||||
class UButton;
|
||||
@@ -48,6 +49,20 @@ public:
|
||||
UFUNCTION(BlueprintCallable, Category = "Dialogue")
|
||||
void Open(const FText& InMessage);
|
||||
|
||||
/**
|
||||
* Meme chose, mais avec un retour automatique passe le delai.
|
||||
*
|
||||
* @param InMessageFormat doit contenir {0}, remplace par les secondes
|
||||
* restantes. Exemple : "Garder ce mode ?\nRetour
|
||||
* automatique dans {0} s."
|
||||
*
|
||||
* A l'expiration, OnCancelled est diffuse -- comme si le joueur avait
|
||||
* refuse. C'est le seul filet quand la question porte sur un changement qui
|
||||
* peut rendre l'ecran illisible.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Dialogue")
|
||||
void OpenWithCountdown(const FText& InMessageFormat, float Seconds);
|
||||
|
||||
/** Referme la boite. Ne previent personne : l'appelant sait ce qu'il fait. */
|
||||
UFUNCTION(BlueprintCallable, Category = "Dialogue")
|
||||
void Close();
|
||||
@@ -82,4 +97,27 @@ private:
|
||||
|
||||
UFUNCTION()
|
||||
void HandleCancelClicked();
|
||||
|
||||
/** Un tick par frame tant que le rebours tourne. Renvoie false pour s'arreter. */
|
||||
bool TickCountdown(float DeltaTime);
|
||||
|
||||
void StopCountdown();
|
||||
|
||||
void RefreshCountdownText();
|
||||
|
||||
/**
|
||||
* Le rebours passe par le ticker du moteur et non par un timer du monde.
|
||||
*
|
||||
* Deux raisons : l'ecran de reglages s'ouvre depuis le menu de PAUSE, ou les
|
||||
* timers du monde sont geles -- le rebours ne repartirait jamais ; et un
|
||||
* changement de resolution rate doit se defaire meme si le jeu bloque.
|
||||
*/
|
||||
FTSTicker::FDelegateHandle CountdownTickerHandle;
|
||||
|
||||
FText CountdownMessageFormat;
|
||||
|
||||
float CountdownRemaining = 0.f;
|
||||
|
||||
/** Evite de reecrire le texte 60 fois par seconde pour rien. */
|
||||
int32 LastDisplayedSeconds = -1;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Blueprint/UserWidget.h"
|
||||
#include "InputCoreTypes.h"
|
||||
#include "KeyCaptureWidget.generated.h"
|
||||
|
||||
class UButton;
|
||||
class UTextBlock;
|
||||
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnKeyCaptured, FKey, CapturedKey);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnKeyCaptureCancelled);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnKeyConflictAnswered, bool, bConfirmed);
|
||||
|
||||
/** Ce que le voile attend a un instant donne. */
|
||||
UENUM()
|
||||
enum class EKeyCaptureState : uint8
|
||||
{
|
||||
/** Ferme. */
|
||||
Idle,
|
||||
|
||||
/** Ouvert, en attente d'une touche. Les boutons sont replies. */
|
||||
Capturing,
|
||||
|
||||
/** Ouvert, la touche est prise : on demande quoi faire. Les boutons apparaissent. */
|
||||
Conflict
|
||||
};
|
||||
|
||||
/**
|
||||
* Le voile de remappage, en deux temps : "Appuie sur une touche", puis
|
||||
* eventuellement "cette touche est deja prise, on la vole ?".
|
||||
*
|
||||
* Les deux etapes vivent dans le MEME widget et non dans deux ecrans successifs.
|
||||
* Le conflit fait partie du geste de remappage : refermer le voile pour ouvrir
|
||||
* une boite par-dessus produirait un clignotement et ferait perdre le fil de ce
|
||||
* qu'on etait en train de modifier. UConfirmDialogWidget garde les questions
|
||||
* generiques -- reinitialiser, abandonner une partie, valider une resolution.
|
||||
*
|
||||
* Classe Abstract : on cree un WBP_KeyCapture qui en herite.
|
||||
*/
|
||||
UCLASS(Abstract)
|
||||
class SURVIVAL_PROJET_API UKeyCaptureWidget : public UUserWidget
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UKeyCaptureWidget(const FObjectInitializer& ObjectInitializer);
|
||||
|
||||
virtual void NativeOnInitialized() override;
|
||||
virtual FReply NativeOnKeyDown(const FGeometry& InGeometry, const FKeyEvent& InKeyEvent) override;
|
||||
virtual FReply NativeOnMouseButtonDown(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent) override;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "Reglages")
|
||||
FOnKeyCaptured OnKeyCaptured;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "Reglages")
|
||||
FOnKeyCaptureCancelled OnCaptureCancelled;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "Reglages")
|
||||
FOnKeyConflictAnswered OnConflictAnswered;
|
||||
|
||||
/** Affiche le voile, ecrit la consigne, replie les boutons et prend le focus. */
|
||||
UFUNCTION(BlueprintCallable, Category = "Reglages")
|
||||
void BeginCapture(const FText& ActionLabel);
|
||||
|
||||
/**
|
||||
* Bascule le voile deja ouvert sur la question de conflit.
|
||||
*
|
||||
* Ne referme rien : c'est tout l'interet d'avoir garde un seul widget.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Reglages")
|
||||
void ShowConflict(const FText& Message);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "Reglages")
|
||||
void EndCapture();
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Reglages")
|
||||
EKeyCaptureState GetCaptureState() const { return State; }
|
||||
|
||||
protected:
|
||||
/** Optionnel : un voile dont le texte est fige dans le Blueprint reste valide. */
|
||||
UPROPERTY(meta = (BindWidgetOptional))
|
||||
TObjectPtr<UTextBlock> PromptText;
|
||||
|
||||
/**
|
||||
* Boutons de la seule etape de conflit. Replies pendant la capture : les
|
||||
* afficher inertes inviterait a cliquer alors qu'on attend une touche.
|
||||
*/
|
||||
UPROPERTY(meta = (BindWidgetOptional))
|
||||
TObjectPtr<UButton> ConfirmButton;
|
||||
|
||||
UPROPERTY(meta = (BindWidgetOptional))
|
||||
TObjectPtr<UButton> CancelButton;
|
||||
|
||||
/** Doit contenir {0}, remplace par le nom de l'action en cours de remappage. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Reglages")
|
||||
FText PromptFormat;
|
||||
|
||||
private:
|
||||
UFUNCTION()
|
||||
void HandleConfirmClicked();
|
||||
|
||||
UFUNCTION()
|
||||
void HandleCancelClicked();
|
||||
|
||||
/** Facteur commun a NativeOnKeyDown et NativeOnMouseButtonDown. */
|
||||
FReply CaptureKey(const FKey& Key);
|
||||
|
||||
/** Affiche ou replie les deux boutons selon l'etat. */
|
||||
void UpdateButtonVisibility();
|
||||
|
||||
EKeyCaptureState State = EKeyCaptureState::Idle;
|
||||
};
|
||||
@@ -0,0 +1,63 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/Info.h"
|
||||
#include "MusicSubsystem.h"
|
||||
#include "MusicDirector.generated.h"
|
||||
|
||||
class UMusicPlaylistDataAsset;
|
||||
|
||||
/**
|
||||
* Declare la musique du jeu. UN SEUL exemplaire suffit, dans la premiere map
|
||||
* chargee -- typiquement la scene de menu.
|
||||
*
|
||||
* Il ne joue rien lui-meme et ne survit pas a sa map : il transmet sa consigne a
|
||||
* UMusicSubsystem, qui, lui, vit sur le GameInstance et traverse tous les
|
||||
* changements de niveau. C'est pour ca que les maps suivantes n'ont besoin de
|
||||
* rien : la musique continue toute seule, et la regle de transition a deja ete
|
||||
* posee ici.
|
||||
*
|
||||
* En poser un second dans une autre map reste possible : il change alors de
|
||||
* playlist a l'arrivee sur celle-la.
|
||||
*
|
||||
* Derive d'AInfo : aucun visuel, aucun tick, et AInfo se declare non
|
||||
* spatialement charge, donc il est present des le demarrage meme en World
|
||||
* Partition -- un acteur ordinaire hors de la zone de streaming ne ferait jamais
|
||||
* son BeginPlay.
|
||||
*/
|
||||
UCLASS(ClassGroup = "Audio", meta = (DisplayName = "Music Director"))
|
||||
class SURVIVAL_PROJET_API AMusicDirector : public AInfo
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
AMusicDirector();
|
||||
|
||||
protected:
|
||||
virtual void BeginPlay() override;
|
||||
|
||||
/**
|
||||
* La playlist a lancer en arrivant sur cette map.
|
||||
*
|
||||
* Si c'est deja elle qui tourne, elle n'est pas relancee. La laisser vide veut
|
||||
* dire "je ne change pas la musique en cours", et n'a d'interet que sur un
|
||||
* second Director pose uniquement pour changer la regle de transition.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Musique")
|
||||
TObjectPtr<UMusicPlaylistDataAsset> Playlist;
|
||||
|
||||
/**
|
||||
* Ce que devient le morceau en cours a CHAQUE changement de scene suivant.
|
||||
*
|
||||
* Continue : il poursuit sa route par-dessus le chargement.
|
||||
* New : il s'eteint et le lecteur en demarre un autre -- tirage aleatoire
|
||||
* ou morceau suivant, selon le mode de la playlist.
|
||||
*
|
||||
* La consigne est envoyee au lecteur, qui la garde : les autres maps n'ont
|
||||
* rien a declarer.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Musique")
|
||||
EMusicLevelChangeMode LevelChangeMode = EMusicLevelChangeMode::Continue;
|
||||
};
|
||||
@@ -0,0 +1,85 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Engine/DataAsset.h"
|
||||
#include "MusicPlaylistDataAsset.generated.h"
|
||||
|
||||
class USoundBase;
|
||||
|
||||
/**
|
||||
* Une liste de morceaux et la facon de les enchainer.
|
||||
*
|
||||
* DataAsset et pas une variable sur un acteur : c'est de la DONNEE, au meme titre
|
||||
* qu'un UItemDataAsset. On en cree autant qu'on veut (menu, exploration, nuit,
|
||||
* combat) sans toucher au code, et la meme playlist peut etre referencee par
|
||||
* plusieurs maps -- ce qui est exactement ce qui permet au menu et au jeu de
|
||||
* partager une musique qui ne se coupe pas entre les deux.
|
||||
*
|
||||
* Aucun etat ici : l'index en cours, l'ordre tire au sort et le volume vivent
|
||||
* dans UMusicSubsystem. Un DataAsset est partage par toutes les references, y
|
||||
* mettre de l'etat le partagerait aussi.
|
||||
*/
|
||||
UCLASS(BlueprintType)
|
||||
class SURVIVAL_PROJET_API UMusicPlaylistDataAsset : public UPrimaryDataAsset
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
/** Nom lisible, pour un futur affichage "en cours de lecture". */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Playlist")
|
||||
FText DisplayName;
|
||||
|
||||
/**
|
||||
* Les morceaux, dans l'ordre de lecture quand l'aleatoire est desactive.
|
||||
*
|
||||
* Les cases vides sont ignorees a la lecture : une reference oubliee ne coupe
|
||||
* pas la musique, elle est simplement sautee.
|
||||
*
|
||||
* IMPORTANT : aucun de ces sons ne doit boucler sur lui-meme (ni "Looping" sur
|
||||
* la SoundWave, ni un noeud Looping dans la SoundCue). L'enchainement repose
|
||||
* sur OnAudioFinished, qui n'est jamais diffuse par un son qui boucle.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Playlist")
|
||||
TArray<TObjectPtr<USoundBase>> Tracks;
|
||||
|
||||
/**
|
||||
* Mode par defaut au demarrage de CETTE playlist.
|
||||
*
|
||||
* Le joueur peut ensuite basculer en cours de route via
|
||||
* UMusicSubsystem::SetShuffleEnabled ; cette valeur n'est relue qu'au
|
||||
* prochain demarrage de playlist.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Playlist")
|
||||
bool bShuffle = true;
|
||||
|
||||
/** Faux = la musique s'arrete quand tous les morceaux ont ete joues une fois. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Playlist")
|
||||
bool bLoop = true;
|
||||
|
||||
/**
|
||||
* Silence entre deux morceaux, tire au hasard dans l'intervalle.
|
||||
*
|
||||
* Une musique d'ambiance qui respire quelques secondes se remarque beaucoup
|
||||
* moins qu'un flux continu, et c'est ce qui evite la lassitude sur une longue
|
||||
* session. A zero, l'enchainement est immediat.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Playlist", meta = (ClampMin = "0.0", Units = "s"))
|
||||
float MinGapBetweenTracks = 0.f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Playlist", meta = (ClampMin = "0.0", Units = "s"))
|
||||
float MaxGapBetweenTracks = 0.f;
|
||||
|
||||
/** Correction de niveau propre a cette playlist, par-dessus le volume joueur. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Playlist", meta = (ClampMin = "0.0", ClampMax = "2.0"))
|
||||
float VolumeMultiplier = 1.f;
|
||||
|
||||
/** Fondu entrant au demarrage de la playlist (pas entre deux morceaux). */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Playlist", meta = (ClampMin = "0.0", Units = "s"))
|
||||
float FadeInDuration = 2.f;
|
||||
|
||||
/** Fondu sortant quand une autre playlist prend la main, ou a l'arret. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Playlist", meta = (ClampMin = "0.0", Units = "s"))
|
||||
float FadeOutDuration = 2.f;
|
||||
};
|
||||
@@ -0,0 +1,204 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Containers/Ticker.h"
|
||||
#include "Subsystems/GameInstanceSubsystem.h"
|
||||
#include "MusicSubsystem.generated.h"
|
||||
|
||||
class UAudioComponent;
|
||||
class UMusicPlaylistDataAsset;
|
||||
class USoundBase;
|
||||
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnMusicTrackChanged, USoundBase*, NewTrack);
|
||||
|
||||
/**
|
||||
* Ce qui arrive au morceau en cours quand on change de scene.
|
||||
*
|
||||
* La question ne se pose que parce que le lecteur, lui, ne s'arrete jamais :
|
||||
* sans consigne, la musique du menu se poursuit telle quelle dans la partie, au
|
||||
* milieu du morceau ou elle en etait.
|
||||
*/
|
||||
UENUM(BlueprintType)
|
||||
enum class EMusicLevelChangeMode : uint8
|
||||
{
|
||||
/** Le morceau en cours poursuit sa route, comme si de rien n'etait. */
|
||||
Continue UMETA(DisplayName = "Continue"),
|
||||
|
||||
/**
|
||||
* Le morceau en cours s'eteint et le suivant demarre : nouveau tirage si
|
||||
* l'aleatoire est actif, morceau suivant de la liste sinon. La playlist, elle,
|
||||
* ne change pas.
|
||||
*/
|
||||
New UMETA(DisplayName = "New")
|
||||
};
|
||||
|
||||
/**
|
||||
* Le lecteur de musique du jeu. Un seul, pour toute la duree de l'application.
|
||||
*
|
||||
* UGameInstanceSubsystem et pas un acteur : il naît avec le GameInstance et meurt
|
||||
* avec le jeu, exactement comme USurvivalGameInstance. Aucune map ne le possede,
|
||||
* donc aucun OpenLevel ne le detruit -- c'est le seul endroit d'ou une musique
|
||||
* peut traverser le passage menu -> partie sans se couper. Le moteur l'instancie
|
||||
* tout seul, il n'y a rien a poser dans une scene ni a cocher nulle part.
|
||||
*
|
||||
* Le composant audio lui-meme est cree via CreateSound2D avec
|
||||
* bPersistAcrossLevelTransition : le moteur le construit alors SANS monde et sans
|
||||
* proprietaire, et lui pose bIgnoreForFlushing -- le drapeau que le vidage du
|
||||
* peripherique audio, declenche a chaque changement de niveau, saute
|
||||
* explicitement. Sans ce parametre le son s'arreterait net au chargement, meme si
|
||||
* l'objet C++, lui, survivait.
|
||||
*
|
||||
* Effet de bord voulu : ces composants sont marques bIsUISound, donc ils
|
||||
* continuent de jouer quand le jeu est en pause. Le menu de pause garde sa
|
||||
* musique sans une ligne de plus.
|
||||
*/
|
||||
UCLASS()
|
||||
class SURVIVAL_PROJET_API UMusicSubsystem : public UGameInstanceSubsystem
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
|
||||
virtual void Deinitialize() override;
|
||||
|
||||
/** Raccourci depuis n'importe quel objet ayant un monde. */
|
||||
UFUNCTION(BlueprintPure, Category = "Musique", meta = (WorldContext = "WorldContextObject", DisplayName = "Get Music Subsystem"))
|
||||
static UMusicSubsystem* Get(const UObject* WorldContextObject);
|
||||
|
||||
/**
|
||||
* Demarre une playlist.
|
||||
*
|
||||
* Si c'est DEJA celle qui joue, ne fait rien : c'est ce comportement qui rend
|
||||
* la transition menu -> jeu inaudible quand les deux maps designent la meme
|
||||
* playlist. Si c'en est une autre, croisement entre l'ancienne qui s'eteint et
|
||||
* la nouvelle qui entre.
|
||||
*
|
||||
* @param bForceRestart Repart du debut meme si la playlist est deja en cours.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Musique")
|
||||
void PlayPlaylist(UMusicPlaylistDataAsset* Playlist, bool bForceRestart = false);
|
||||
|
||||
/** @param FadeOutDuration Negatif = utiliser le fondu declare par la playlist. */
|
||||
UFUNCTION(BlueprintCallable, Category = "Musique")
|
||||
void StopMusic(float FadeOutDuration = -1.f);
|
||||
|
||||
/** Passe au morceau suivant, dans l'ordre courant (aleatoire ou non). */
|
||||
UFUNCTION(BlueprintCallable, Category = "Musique")
|
||||
void SkipToNextTrack();
|
||||
|
||||
/**
|
||||
* Bascule aleatoire / ordre de la liste.
|
||||
*
|
||||
* Ne coupe jamais le morceau en cours : seul l'enchainement change. Le curseur
|
||||
* se recale sur le morceau qui joue, donc en repassant en mode ordonne la
|
||||
* lecture reprend juste apres lui au lieu de sauter au debut de la liste.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Musique")
|
||||
void SetShuffleEnabled(bool bEnabled);
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Musique")
|
||||
bool IsShuffleEnabled() const { return bShuffleEnabled; }
|
||||
|
||||
/**
|
||||
* Ce que devient le morceau en cours au prochain changement de scene.
|
||||
*
|
||||
* La consigne vit ICI et pas sur l'acteur qui l'a posee : l'acteur meurt avec
|
||||
* sa map, alors que le changement de scene se produit justement apres. Une
|
||||
* seule map -- le menu -- suffit donc a la declarer pour tout le jeu.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Musique")
|
||||
void SetLevelChangeMode(EMusicLevelChangeMode Mode) { LevelChangeMode = Mode; }
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Musique")
|
||||
EMusicLevelChangeMode GetLevelChangeMode() const { return LevelChangeMode; }
|
||||
|
||||
/** Volume joueur, 0 a 1. A brancher sur l'onglet Audio des reglages plus tard. */
|
||||
UFUNCTION(BlueprintCallable, Category = "Musique")
|
||||
void SetMusicVolume(float NewVolume);
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Musique")
|
||||
float GetMusicVolume() const { return MusicVolume; }
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Musique")
|
||||
bool IsPlaying() const { return bIsPlaying; }
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Musique")
|
||||
USoundBase* GetCurrentTrack() const;
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Musique")
|
||||
UMusicPlaylistDataAsset* GetCurrentPlaylist() const { return CurrentPlaylist; }
|
||||
|
||||
/** Diffuse a chaque changement de morceau, nullptr a l'arret. */
|
||||
UPROPERTY(BlueprintAssignable, Category = "Musique")
|
||||
FOnMusicTrackChanged OnMusicTrackChanged;
|
||||
|
||||
private:
|
||||
/** Fin NATURELLE d'un morceau uniquement -- voir FadeOutAndRelease. */
|
||||
UFUNCTION()
|
||||
void HandleTrackFinished();
|
||||
|
||||
/** Applique LevelChangeMode a chaque map chargee. */
|
||||
void HandlePostLoadMap(UWorld* LoadedWorld);
|
||||
|
||||
/** (Re)construit l'ordre de lecture du cycle a venir. */
|
||||
void BuildPlayOrder();
|
||||
|
||||
void AdvanceAndPlay(float FadeInDuration);
|
||||
void PlayTrackAtCursor(float FadeInDuration);
|
||||
UAudioComponent* CreateMusicComponent(USoundBase* Sound);
|
||||
|
||||
/**
|
||||
* Detache puis eteint un composant : apres cet appel, plus rien de ce qu'il
|
||||
* diffuse ne revient vers nous.
|
||||
*/
|
||||
void FadeOutAndRelease(TObjectPtr<UAudioComponent>& Component, float FadeOutDuration);
|
||||
|
||||
void ScheduleGap(float DelaySeconds);
|
||||
void CancelPendingGap();
|
||||
float GetEffectiveVolume() const;
|
||||
|
||||
UPROPERTY()
|
||||
TObjectPtr<UMusicPlaylistDataAsset> CurrentPlaylist;
|
||||
|
||||
/**
|
||||
* UPROPERTY obligatoire : ces composants n'ont ni monde ni acteur proprietaire,
|
||||
* cette reference est la SEULE qui les tienne hors de portee du ramasse-miettes.
|
||||
*/
|
||||
UPROPERTY()
|
||||
TObjectPtr<UAudioComponent> MusicComponent;
|
||||
|
||||
/** La voie de sortie, le temps d'un croisement. Une seule a la fois. */
|
||||
UPROPERTY()
|
||||
TObjectPtr<UAudioComponent> FadingOutComponent;
|
||||
|
||||
/**
|
||||
* Index dans Tracks, deja melangee si l'aleatoire est actif.
|
||||
*
|
||||
* Un ordre pre-melange plutot qu'un tirage a chaque morceau : le tirage
|
||||
* independant rejoue le meme titre deux fois de suite une fois sur N, et sur
|
||||
* une playlist de cinq morceaux ca s'entend tout de suite.
|
||||
*/
|
||||
TArray<int32> PlayOrder;
|
||||
|
||||
int32 OrderCursor = INDEX_NONE;
|
||||
int32 LastPlayedTrackIndex = INDEX_NONE;
|
||||
|
||||
bool bShuffleEnabled = true;
|
||||
bool bIsPlaying = false;
|
||||
float MusicVolume = 1.f;
|
||||
|
||||
EMusicLevelChangeMode LevelChangeMode = EMusicLevelChangeMode::Continue;
|
||||
|
||||
/** Abonnement au chargement de map, retire a la fermeture du jeu. */
|
||||
FDelegateHandle PostLoadMapHandle;
|
||||
|
||||
/**
|
||||
* Le silence entre deux morceaux passe par le ticker de Core, pas par un
|
||||
* FTimerHandle : un timer appartient au monde, donc il disparaîtrait au
|
||||
* chargement de la map suivante -- et il ne tourne pas en pause. Le ticker de
|
||||
* Core ignore les deux.
|
||||
*/
|
||||
FTSTicker::FDelegateHandle GapTickerHandle;
|
||||
};
|
||||
@@ -0,0 +1,73 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Blueprint/UserWidget.h"
|
||||
#include "InputCoreTypes.h"
|
||||
#include "SettingKeyRowWidget.generated.h"
|
||||
|
||||
class UButton;
|
||||
class UTextBlock;
|
||||
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnSettingKeyRebindRequested, FName, MappingName);
|
||||
|
||||
/**
|
||||
* Une ligne de remappage : un libelle, et un bouton portant la touche actuelle.
|
||||
*
|
||||
* Widget distinct de USettingRowWidget et non un mode de plus : une touche n'est
|
||||
* pas un index dans une liste de paliers, il n'y a ni fleches ni curseur, et
|
||||
* l'interaction est d'une autre nature -- on ouvre un voile de capture.
|
||||
*
|
||||
* Elle ne sait pas remapper. Elle previent via OnRebindRequested et c'est
|
||||
* l'onglet qui pilote, comme partout ailleurs dans les reglages.
|
||||
*
|
||||
* Classe Abstract : on cree un WBP_SettingKeyRow qui en herite.
|
||||
*/
|
||||
UCLASS(Abstract)
|
||||
class SURVIVAL_PROJET_API USettingKeyRowWidget : public UUserWidget
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
virtual void NativeOnInitialized() override;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "Reglages")
|
||||
FOnSettingKeyRebindRequested OnRebindRequested;
|
||||
|
||||
/** Pose le libelle et la touche. Le nom de mapping est renvoye tel quel au clic. */
|
||||
UFUNCTION(BlueprintCallable, Category = "Reglages")
|
||||
void Setup(FName InMappingName, const FText& Label, const FKey& Key);
|
||||
|
||||
/** Met a jour la seule touche affichee. */
|
||||
UFUNCTION(BlueprintCallable, Category = "Reglages")
|
||||
void SetKey(const FKey& Key);
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Reglages")
|
||||
FName GetMappingName() const { return MappingName; }
|
||||
|
||||
protected:
|
||||
UPROPERTY(meta = (BindWidget))
|
||||
TObjectPtr<UTextBlock> LabelText;
|
||||
|
||||
UPROPERTY(meta = (BindWidget))
|
||||
TObjectPtr<UButton> KeyButton;
|
||||
|
||||
UPROPERTY(meta = (BindWidget))
|
||||
TObjectPtr<UTextBlock> KeyText;
|
||||
|
||||
/** Couleur du texte quand l'action n'a plus aucune touche. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Reglages")
|
||||
FLinearColor UnboundColor = FLinearColor(0.9f, 0.2f, 0.2f);
|
||||
|
||||
/** Couleur normale. Reprise telle quelle a la premiere affectation valide. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Reglages")
|
||||
FLinearColor BoundColor = FLinearColor::White;
|
||||
|
||||
private:
|
||||
UFUNCTION()
|
||||
void HandleKeyButtonClicked();
|
||||
|
||||
/** Identifiant Enhanced Input du mapping, pas le libelle affiche. */
|
||||
FName MappingName = NAME_None;
|
||||
};
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
class UButton;
|
||||
class UProgressBar;
|
||||
class USlider;
|
||||
class UTextBlock;
|
||||
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnSettingRowIndexChanged, int32, NewIndex);
|
||||
@@ -49,10 +50,17 @@ public:
|
||||
|
||||
/**
|
||||
* Mode nombre : la valeur affichee est calculee a partir de l'index.
|
||||
* Suffix sert aux unites ("%", "°") et peut rester vide.
|
||||
*
|
||||
* @param Step pas fin, celui du glissement. C'est lui qui definit la
|
||||
* granularite de l'index.
|
||||
* @param ArrowStep bond des fleches, forcement un multiple de Step. Les
|
||||
* fleches se calent sur le multiple suivant plutot que
|
||||
* d'ajouter betement leur bond : depuis 37 avec un bond de
|
||||
* 5 on arrive a 40, pas a 42.
|
||||
* @param Suffix unite ("%", "°"), peut rester vide.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Reglages")
|
||||
void SetupAsNumber(const FText& Label, float Min, float Max, float Step, float Value, const FText& Suffix);
|
||||
void SetupAsNumber(const FText& Label, float Min, float Max, float Step, float ArrowStep, float Value, const FText& Suffix);
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Reglages")
|
||||
int32 GetIndex() const { return CurrentIndex; }
|
||||
@@ -75,6 +83,18 @@ public:
|
||||
UFUNCTION(BlueprintCallable, Category = "Reglages")
|
||||
void SetNumericValueSilently(float Value);
|
||||
|
||||
/**
|
||||
* Derniere valeur que le JOUEUR peut atteindre, les suivantes restant
|
||||
* affichables par le code.
|
||||
*
|
||||
* Sert a "Personnalise" en qualite globale : c'est un etat dans lequel on
|
||||
* tombe en bougeant un curseur individuel, jamais un choix qu'on fait.
|
||||
* Sans ca la fleche droite proposerait de le "selectionner", ce qui ne veut
|
||||
* rien dire. INDEX_NONE pour lever la limite.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Reglages")
|
||||
void SetMaxSelectableIndex(int32 InMaxSelectableIndex);
|
||||
|
||||
protected:
|
||||
UPROPERTY(meta = (BindWidget))
|
||||
TObjectPtr<UTextBlock> LabelText;
|
||||
@@ -89,14 +109,29 @@ protected:
|
||||
TObjectPtr<UButton> RightArrowButton;
|
||||
|
||||
/**
|
||||
* Facultative : une ligne de choix (Maintien / Bascule) n'a rien a remplir.
|
||||
* Volontairement une ProgressBar et non un Slider : la barre montre ou l'on
|
||||
* en est, ce sont les fleches qui reglent. Un slider glissable produirait des
|
||||
* valeurs entre deux paliers que le setter refuserait ensuite.
|
||||
* Le REMPLISSAGE visible, et rien d'autre.
|
||||
*
|
||||
* Le Slider d'Unreal ne sait pas dessiner de portion remplie : il rend une
|
||||
* barre uniforme plus une poignee. On empile donc les deux -- cette
|
||||
* ProgressBar pour le rendu, le Slider transparent par-dessus pour le
|
||||
* geste. C'est le montage habituel, pas un bricolage.
|
||||
*/
|
||||
UPROPERTY(meta = (BindWidgetOptional))
|
||||
TObjectPtr<UProgressBar> ValueBar;
|
||||
|
||||
/**
|
||||
* Le GESTE, et rien d'autre : a styler entierement transparent dans le WBP.
|
||||
*
|
||||
* Sa course est exprimee en INDEX de palier (0 a StepCount-1) et non dans
|
||||
* l'unite du reglage -- c'est ce qui garantit qu'un glissement ne peut pas
|
||||
* produire une valeur hors palier, que le setter refuserait ensuite.
|
||||
*
|
||||
* Les deux sont facultatifs : une ligne de choix (Maintien / Bascule) les
|
||||
* replie tous les deux, il n'y a rien a doser.
|
||||
*/
|
||||
UPROPERTY(meta = (BindWidgetOptional))
|
||||
TObjectPtr<USlider> ValueSlider;
|
||||
|
||||
private:
|
||||
UFUNCTION()
|
||||
void HandleLeftClicked();
|
||||
@@ -104,9 +139,19 @@ private:
|
||||
UFUNCTION()
|
||||
void HandleRightClicked();
|
||||
|
||||
/** Glissement souris : on arrondit sur le palier le plus proche. */
|
||||
UFUNCTION()
|
||||
void HandleSliderValueChanged(float NewValue);
|
||||
|
||||
/** Deplace l'index de Delta paliers et diffuse, si le mouvement est possible. */
|
||||
void StepIndex(int32 Delta);
|
||||
|
||||
/** Index vise par une fleche : le multiple de son bond suivant ou precedent. */
|
||||
int32 ArrowTargetIndex(int32 Direction) const;
|
||||
|
||||
/** Plus grand index atteignable par le joueur. */
|
||||
int32 MaxSelectable() const;
|
||||
|
||||
/** Reecrit le texte, la barre et l'etat des deux fleches. */
|
||||
void Refresh();
|
||||
|
||||
@@ -120,8 +165,23 @@ private:
|
||||
|
||||
int32 CurrentIndex = 0;
|
||||
|
||||
/** Nombre de paliers, donc d'index valides. Toujours au moins 1. */
|
||||
/** Nombre de paliers fins, donc d'index valides. Toujours au moins 1. */
|
||||
int32 StepCount = 1;
|
||||
|
||||
/** Bond des fleches, exprime en nombre de paliers fins. 1 en mode choix. */
|
||||
int32 ArrowIndexDelta = 1;
|
||||
|
||||
/** INDEX_NONE = aucune limite, tous les paliers sont selectionnables. */
|
||||
int32 MaxSelectableIndex = INDEX_NONE;
|
||||
|
||||
bool bNumeric = false;
|
||||
|
||||
/**
|
||||
* Vrai pendant qu'on repositionne le curseur depuis le code.
|
||||
*
|
||||
* USlider::SetValue ne rediffuse pas son delegue aujourd'hui, mais s'en
|
||||
* remettre a ce detail d'implementation coute une boucle infinie le jour ou
|
||||
* il change.
|
||||
*/
|
||||
bool bRefreshingSlider = false;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "InputCoreTypes.h"
|
||||
#include "SettingsTabWidget.h"
|
||||
#include "SettingsControlsWidget.generated.h"
|
||||
|
||||
class UEnhancedInputUserSettings;
|
||||
class USettingKeyRowWidget;
|
||||
|
||||
/**
|
||||
* L'onglet "Touches".
|
||||
*
|
||||
* Le stockage, la persistance et l'application des surcharges appartiennent a
|
||||
* UEnhancedInputUserSettings, natif depuis 5.3 : on n'ecrit ni serialisation ni
|
||||
* reconstruction d'IMC. Cet onglet n'est qu'une facade -- afficher les touches
|
||||
* courantes, capturer la nouvelle, arbitrer les conflits.
|
||||
*
|
||||
* PREREQUIS EDITEUR : sans "Enable User Settings" dans Project Settings ->
|
||||
* Enhanced Input, et sans Player Mappable Key Settings sur chaque mapping de
|
||||
* IMC_Default, toutes les lignes afficheront "Non assignée".
|
||||
*
|
||||
* Classe Abstract : on cree un WBP_SettingsControls qui en herite.
|
||||
*/
|
||||
UCLASS(Abstract)
|
||||
class SURVIVAL_PROJET_API USettingsControlsWidget : public USettingsTabWidget
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
USettingsControlsWidget(const FObjectInitializer& ObjectInitializer);
|
||||
|
||||
virtual void NativeConstruct() override;
|
||||
|
||||
virtual void RefreshFromSettings() override;
|
||||
virtual void ResetToDefaults() override;
|
||||
|
||||
protected:
|
||||
UPROPERTY(meta = (BindWidget))
|
||||
TObjectPtr<USettingKeyRowWidget> MoveForwardRow;
|
||||
|
||||
UPROPERTY(meta = (BindWidget))
|
||||
TObjectPtr<USettingKeyRowWidget> MoveBackwardRow;
|
||||
|
||||
UPROPERTY(meta = (BindWidget))
|
||||
TObjectPtr<USettingKeyRowWidget> MoveLeftRow;
|
||||
|
||||
UPROPERTY(meta = (BindWidget))
|
||||
TObjectPtr<USettingKeyRowWidget> MoveRightRow;
|
||||
|
||||
UPROPERTY(meta = (BindWidget))
|
||||
TObjectPtr<USettingKeyRowWidget> JumpRow;
|
||||
|
||||
UPROPERTY(meta = (BindWidget))
|
||||
TObjectPtr<USettingKeyRowWidget> SprintRow;
|
||||
|
||||
UPROPERTY(meta = (BindWidget))
|
||||
TObjectPtr<USettingKeyRowWidget> CrouchRow;
|
||||
|
||||
UPROPERTY(meta = (BindWidget))
|
||||
TObjectPtr<USettingKeyRowWidget> InteractRow;
|
||||
|
||||
UPROPERTY(meta = (BindWidget))
|
||||
TObjectPtr<USettingKeyRowWidget> UseItemRow;
|
||||
|
||||
UPROPERTY(meta = (BindWidget))
|
||||
TObjectPtr<USettingKeyRowWidget> ToggleInventoryRow;
|
||||
|
||||
UPROPERTY(meta = (BindWidget))
|
||||
TObjectPtr<USettingKeyRowWidget> DropItemRow;
|
||||
|
||||
/**
|
||||
* Question posee en cas de conflit. Trois emplacements :
|
||||
* {0} la touche, {1} l'action qui la possede, {2} l'action qui la reclame.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Reglages")
|
||||
FText ConflictMessage;
|
||||
|
||||
private:
|
||||
UFUNCTION()
|
||||
void HandleRebindRequested(FName MappingName);
|
||||
|
||||
UFUNCTION()
|
||||
void HandleKeyCaptured(FKey CapturedKey);
|
||||
|
||||
UFUNCTION()
|
||||
void HandleConflictAnswered(bool bConfirmed);
|
||||
|
||||
/** Ecrit la touche en attente sur le mapping en attente, puis rafraichit. */
|
||||
void ApplyPendingRebind();
|
||||
|
||||
/** Le sous-systeme de reglages d'input du joueur local, ou nullptr. */
|
||||
UEnhancedInputUserSettings* GetInputSettings() const;
|
||||
|
||||
/** Toutes les lignes, dans l'ordre d'affichage. Ignore les nulles. */
|
||||
TArray<USettingKeyRowWidget*> GetAllRows() const;
|
||||
|
||||
/** La ligne portant ce nom de mapping, ou nullptr. */
|
||||
USettingKeyRowWidget* FindRow(FName MappingName) const;
|
||||
|
||||
/**
|
||||
* Donne son identite a une ligne et retient son libelle.
|
||||
*
|
||||
* Le libelle vient du C++ et non du "Display Name" des Player Mappable Key
|
||||
* Settings : ca t'evite de le saisir onze fois dans l'editeur, et ca garde
|
||||
* les textes traduisibles au meme endroit que le reste du menu.
|
||||
*/
|
||||
void SetupRow(USettingKeyRowWidget* Row, FName MappingName, const FText& Label);
|
||||
|
||||
/** Libelle lisible d'un mapping. Retombe sur l'identifiant si inconnu. */
|
||||
FText GetMappingDisplayName(FName MappingName) const;
|
||||
|
||||
/** Libelles retenus a la construction, pour les messages de conflit. */
|
||||
TMap<FName, FText> MappingLabels;
|
||||
|
||||
/** Mapping en cours de remappage, et touche capturee en attente d'arbitrage. */
|
||||
FName PendingMappingName = NAME_None;
|
||||
|
||||
FKey PendingKey;
|
||||
|
||||
/** Mapping a qui l'on volerait la touche si le joueur confirme. */
|
||||
FName ConflictingMappingName = NAME_None;
|
||||
};
|
||||
@@ -0,0 +1,121 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "SettingsTabWidget.h"
|
||||
#include "SettingsDisplayWidget.generated.h"
|
||||
|
||||
class USettingRowWidget;
|
||||
class USurvivalUserSettings;
|
||||
|
||||
/**
|
||||
* L'onglet "Affichage".
|
||||
*
|
||||
* Presque tout est deja ecrit dans UGameUserSettings : mode d'affichage,
|
||||
* resolution, VSync, limite de FPS, echelle de resolution. On ne fait que
|
||||
* l'habiller. Seule la luminosite nous appartient, parce que le gamma vit sur
|
||||
* le moteur et non dans les reglages.
|
||||
*
|
||||
* Le changement de resolution ou de mode passe par une CONFIRMATION A REBOURS :
|
||||
* c'est le seul filet contre un mode que l'ecran ne sait pas afficher, situation
|
||||
* dont on ne peut sinon sortir qu'en supprimant GameUserSettings.ini a la main.
|
||||
*
|
||||
* Classe Abstract : on cree un WBP_SettingsDisplay qui en herite.
|
||||
*/
|
||||
UCLASS(Abstract)
|
||||
class SURVIVAL_PROJET_API USettingsDisplayWidget : public USettingsTabWidget
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
USettingsDisplayWidget(const FObjectInitializer& ObjectInitializer);
|
||||
|
||||
virtual void NativeConstruct() override;
|
||||
|
||||
virtual void RefreshFromSettings() override;
|
||||
virtual void ResetToDefaults() override;
|
||||
|
||||
protected:
|
||||
UPROPERTY(meta = (BindWidget))
|
||||
TObjectPtr<USettingRowWidget> DisplayModeRow;
|
||||
|
||||
UPROPERTY(meta = (BindWidget))
|
||||
TObjectPtr<USettingRowWidget> ResolutionRow;
|
||||
|
||||
UPROPERTY(meta = (BindWidget))
|
||||
TObjectPtr<USettingRowWidget> FrameRateLimitRow;
|
||||
|
||||
UPROPERTY(meta = (BindWidget))
|
||||
TObjectPtr<USettingRowWidget> VSyncRow;
|
||||
|
||||
UPROPERTY(meta = (BindWidget))
|
||||
TObjectPtr<USettingRowWidget> ResolutionScaleRow;
|
||||
|
||||
UPROPERTY(meta = (BindWidget))
|
||||
TObjectPtr<USettingRowWidget> BrightnessRow;
|
||||
|
||||
/** Doit contenir {0}, remplace par les secondes restantes. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Reglages")
|
||||
FText VideoConfirmMessage;
|
||||
|
||||
/** Delai avant retour automatique au mode precedent. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Reglages", meta = (ClampMin = "3", ClampMax = "30"))
|
||||
float VideoConfirmSeconds = 10.f;
|
||||
|
||||
/**
|
||||
* Proposer aussi les formats ultralarges (21:9) en plus du 16:9.
|
||||
*
|
||||
* La liste que renvoie le systeme contient TOUT ce que la carte accepte :
|
||||
* 4:3, 5:4, 16:10, des modes d'un autre age. On n'en garde que deux familles.
|
||||
* Le 16:9 est toujours accepte -- il represente plus des trois quarts des
|
||||
* joueurs Steam a lui seul. Le 21:9 est marginal mais reel, d'ou l'option.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Reglages")
|
||||
bool bAllowUltrawide = true;
|
||||
|
||||
/** Plus petite resolution proposee. En dessous, le HUD devient illisible. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Reglages")
|
||||
FIntPoint MinimumResolution = FIntPoint(1280, 720);
|
||||
|
||||
private:
|
||||
UFUNCTION()
|
||||
void HandleDisplayModeChanged(int32 NewIndex);
|
||||
|
||||
UFUNCTION()
|
||||
void HandleResolutionChanged(int32 NewIndex);
|
||||
|
||||
UFUNCTION()
|
||||
void HandleFrameRateLimitChanged(int32 NewIndex);
|
||||
|
||||
UFUNCTION()
|
||||
void HandleVSyncChanged(int32 NewIndex);
|
||||
|
||||
UFUNCTION()
|
||||
void HandleResolutionScaleChanged(int32 NewIndex);
|
||||
|
||||
UFUNCTION()
|
||||
void HandleBrightnessChanged(int32 NewIndex);
|
||||
|
||||
/** Reponse a la question posee apres un changement de mode ou de resolution. */
|
||||
UFUNCTION()
|
||||
void HandleVideoConfirmAnswered(bool bConfirmed);
|
||||
|
||||
/** Applique le mode video puis demande confirmation. */
|
||||
void ApplyVideoModeAndConfirm();
|
||||
|
||||
/** Recharge la liste des resolutions de l'ecran et repositionne la ligne. */
|
||||
void RebuildResolutionList();
|
||||
|
||||
/** Ecarte les formats non desires. Ne vide jamais la liste completement. */
|
||||
void FilterResolutions(const FIntPoint& CurrentResolution);
|
||||
|
||||
/** Index de la resolution courante dans AvailableResolutions, 0 si inconnue. */
|
||||
int32 FindCurrentResolutionIndex(const USurvivalUserSettings* Settings) const;
|
||||
|
||||
/** Index de la plus grande resolution proposee, INDEX_NONE si la liste est vide. */
|
||||
int32 FindLargestResolutionIndex() const;
|
||||
|
||||
/** Resolutions proposees, dans l'ordre de la ligne. */
|
||||
TArray<FIntPoint> AvailableResolutions;
|
||||
};
|
||||
@@ -0,0 +1,111 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "SettingsTabWidget.h"
|
||||
#include "SettingsGraphicsWidget.generated.h"
|
||||
|
||||
class USettingRowWidget;
|
||||
class USurvivalUserSettings;
|
||||
|
||||
/**
|
||||
* L'onglet "Graphismes" : les dix curseurs de scalability du moteur, plus une
|
||||
* qualite globale qui les pousse tous.
|
||||
*
|
||||
* Zero logique metier ici : chaque ligne appelle un Set...Quality(0-3) de
|
||||
* UGameUserSettings, le moteur fait le reste. C'est l'onglet qui rapporte le
|
||||
* plus pour le moins d'effort.
|
||||
*
|
||||
* Vegetation et Distance d'affichage sont places EN HAUT et non dans l'ordre du
|
||||
* moteur : avec SoStylized, ce sont les deux seuls qui decident vraiment si le
|
||||
* jeu tourne sur une machine moyenne.
|
||||
*
|
||||
* Classe Abstract : on cree un WBP_SettingsGraphics qui en herite.
|
||||
*/
|
||||
UCLASS(Abstract)
|
||||
class SURVIVAL_PROJET_API USettingsGraphicsWidget : public USettingsTabWidget
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
virtual void NativeConstruct() override;
|
||||
|
||||
virtual void RefreshFromSettings() override;
|
||||
virtual void ResetToDefaults() override;
|
||||
|
||||
protected:
|
||||
/** Bas / Moyen / Élevé / Épique, plus "Personnalisé" que le joueur ne peut pas choisir. */
|
||||
UPROPERTY(meta = (BindWidget))
|
||||
TObjectPtr<USettingRowWidget> OverallQualityRow;
|
||||
|
||||
UPROPERTY(meta = (BindWidget))
|
||||
TObjectPtr<USettingRowWidget> ViewDistanceRow;
|
||||
|
||||
UPROPERTY(meta = (BindWidget))
|
||||
TObjectPtr<USettingRowWidget> FoliageRow;
|
||||
|
||||
UPROPERTY(meta = (BindWidget))
|
||||
TObjectPtr<USettingRowWidget> ShadowRow;
|
||||
|
||||
UPROPERTY(meta = (BindWidget))
|
||||
TObjectPtr<USettingRowWidget> TextureRow;
|
||||
|
||||
UPROPERTY(meta = (BindWidget))
|
||||
TObjectPtr<USettingRowWidget> VisualEffectRow;
|
||||
|
||||
UPROPERTY(meta = (BindWidget))
|
||||
TObjectPtr<USettingRowWidget> PostProcessRow;
|
||||
|
||||
UPROPERTY(meta = (BindWidget))
|
||||
TObjectPtr<USettingRowWidget> AntiAliasingRow;
|
||||
|
||||
UPROPERTY(meta = (BindWidget))
|
||||
TObjectPtr<USettingRowWidget> ReflectionRow;
|
||||
|
||||
UPROPERTY(meta = (BindWidget))
|
||||
TObjectPtr<USettingRowWidget> GlobalIlluminationRow;
|
||||
|
||||
UPROPERTY(meta = (BindWidget))
|
||||
TObjectPtr<USettingRowWidget> ShadingRow;
|
||||
|
||||
private:
|
||||
UFUNCTION()
|
||||
void HandleOverallQualityChanged(int32 NewIndex);
|
||||
|
||||
UFUNCTION()
|
||||
void HandleViewDistanceChanged(int32 NewIndex);
|
||||
|
||||
UFUNCTION()
|
||||
void HandleFoliageChanged(int32 NewIndex);
|
||||
|
||||
UFUNCTION()
|
||||
void HandleShadowChanged(int32 NewIndex);
|
||||
|
||||
UFUNCTION()
|
||||
void HandleTextureChanged(int32 NewIndex);
|
||||
|
||||
UFUNCTION()
|
||||
void HandleVisualEffectChanged(int32 NewIndex);
|
||||
|
||||
UFUNCTION()
|
||||
void HandlePostProcessChanged(int32 NewIndex);
|
||||
|
||||
UFUNCTION()
|
||||
void HandleAntiAliasingChanged(int32 NewIndex);
|
||||
|
||||
UFUNCTION()
|
||||
void HandleReflectionChanged(int32 NewIndex);
|
||||
|
||||
UFUNCTION()
|
||||
void HandleGlobalIlluminationChanged(int32 NewIndex);
|
||||
|
||||
UFUNCTION()
|
||||
void HandleShadingChanged(int32 NewIndex);
|
||||
|
||||
/** Prepare une ligne de qualite : memes quatre libelles partout. */
|
||||
void SetupQualityRow(USettingRowWidget* Row, const FText& Label);
|
||||
|
||||
/** Pousse la scalability et rafraichit la ligne de qualite globale. */
|
||||
void ApplyAndRefreshOverall();
|
||||
};
|
||||
@@ -8,11 +8,18 @@
|
||||
|
||||
class UButton;
|
||||
class UConfirmDialogWidget;
|
||||
class UKeyCaptureWidget;
|
||||
class USettingsTabWidget;
|
||||
class UWidgetSwitcher;
|
||||
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnSettingsCloseRequested);
|
||||
|
||||
/** Reponse a une demande de confirmation. bConfirmed est faux sur un refus ou une expiration. */
|
||||
DECLARE_DYNAMIC_DELEGATE_OneParam(FOnSettingsConfirmationResult, bool, bConfirmed);
|
||||
|
||||
/** Touche capturee. Invalide si le joueur a annule avec Echap. */
|
||||
DECLARE_DYNAMIC_DELEGATE_OneParam(FOnSettingsKeyCaptureResult, FKey, CapturedKey);
|
||||
|
||||
/**
|
||||
* L'ecran de reglages : une barre d'onglets, un WidgetSwitcher, Retour et
|
||||
* Reinitialiser.
|
||||
@@ -55,6 +62,57 @@ public:
|
||||
UFUNCTION(BlueprintCallable, Category = "Reglages")
|
||||
void Close();
|
||||
|
||||
/**
|
||||
* ZOrder de la boite de confirmation, impose par celui qui ouvre l'ecran.
|
||||
*
|
||||
* L'ecran ne le choisit PAS lui-meme : il est ouvert depuis deux contextes
|
||||
* dont les empilements n'ont rien a voir (50 au menu principal, 150 en jeu).
|
||||
* Un numero code en dur ici obligerait le widget a savoir qui l'affiche,
|
||||
* exactement ce qu'on evite avec OnCloseRequested. A appeler AVANT Open().
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Reglages")
|
||||
void SetDialogZOrder(int32 InZOrder) { DialogZOrder = InZOrder; }
|
||||
|
||||
/**
|
||||
* Pose une question au joueur pour le compte d'un onglet.
|
||||
*
|
||||
* L'ecran est le seul a posseder une boite : un onglet qui creerait la
|
||||
* sienne devrait a son tour se demander a quel ZOrder la poser, et on
|
||||
* repartirait pour un tour.
|
||||
*
|
||||
* @param MessageOrFormat texte simple, ou format contenant {0} si
|
||||
* CountdownSeconds est superieur a zero.
|
||||
* @param CountdownSeconds 0 pour une question sans limite de temps.
|
||||
* @return false si aucune boite n'est configuree -- l'appelant doit alors
|
||||
* decider seul, sa reponse n'arrivera jamais.
|
||||
*/
|
||||
bool RequestConfirmation(const FText& MessageOrFormat, float CountdownSeconds, const FOnSettingsConfirmationResult& OnResult);
|
||||
|
||||
/**
|
||||
* Ouvre le voile de capture de touche pour le compte d'un onglet.
|
||||
*
|
||||
* Meme raison que la boite : un voile pose DANS l'onglet serait clippe a la
|
||||
* zone du WidgetSwitcher au lieu de couvrir l'ecran, et devrait a son tour
|
||||
* se trouver un ZOrder.
|
||||
*
|
||||
* @return false si aucun voile n'est configure -- la reponse n'arrivera
|
||||
* jamais, l'appelant doit renoncer.
|
||||
*/
|
||||
bool RequestKeyCapture(const FText& ActionLabel, const FOnSettingsKeyCaptureResult& OnResult);
|
||||
|
||||
/**
|
||||
* Bascule le voile deja ouvert sur la question de conflit.
|
||||
*
|
||||
* Le voile RESTE ouvert entre la capture et la reponse : c'est le meme geste
|
||||
* pour le joueur, il n'a pas a le voir disparaitre puis revenir.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Reglages")
|
||||
void ShowKeyCaptureConflict(const FText& Message, const FOnSettingsConfirmationResult& OnAnswer);
|
||||
|
||||
/** Referme le voile. A appeler quand le remappage est termine ou abandonne. */
|
||||
UFUNCTION(BlueprintCallable, Category = "Reglages")
|
||||
void CloseKeyCapture();
|
||||
|
||||
protected:
|
||||
UPROPERTY(meta = (BindWidget))
|
||||
TObjectPtr<UWidgetSwitcher> TabSwitcher;
|
||||
@@ -62,41 +120,58 @@ protected:
|
||||
UPROPERTY(meta = (BindWidget))
|
||||
TObjectPtr<UButton> BackButton;
|
||||
|
||||
UPROPERTY(meta = (BindWidgetOptional))
|
||||
UPROPERTY(meta = (BindWidget))
|
||||
TObjectPtr<UButton> ResetButton;
|
||||
|
||||
/**
|
||||
* Les onglets a venir. Grises automatiquement tant que le WidgetSwitcher ne
|
||||
* contient pas la page correspondante -- meme principe que les boutons de
|
||||
* coop du menu principal : ils annoncent la couleur sans mener nulle part.
|
||||
* Les trois onglets qui existent sont OBLIGATOIRES : un WBP_Settings sans eux
|
||||
* ne compile pas.
|
||||
*
|
||||
* BindWidgetOptional echoue en silence -- un nom mal orthographie donnait un
|
||||
* bouton parfaitement inerte, sans le moindre message. Pour un element dont
|
||||
* l'absence n'a aucun sens, on veut l'erreur a la compilation du Blueprint.
|
||||
*/
|
||||
UPROPERTY(meta = (BindWidgetOptional))
|
||||
UPROPERTY(meta = (BindWidget))
|
||||
TObjectPtr<UButton> GameplayTabButton;
|
||||
|
||||
UPROPERTY(meta = (BindWidgetOptional))
|
||||
UPROPERTY(meta = (BindWidget))
|
||||
TObjectPtr<UButton> DisplayTabButton;
|
||||
|
||||
UPROPERTY(meta = (BindWidgetOptional))
|
||||
UPROPERTY(meta = (BindWidget))
|
||||
TObjectPtr<UButton> GraphicsTabButton;
|
||||
|
||||
UPROPERTY(meta = (BindWidget))
|
||||
TObjectPtr<UButton> ControlsTabButton;
|
||||
|
||||
/**
|
||||
* Celui-la reste optionnel : sa page n'existe pas encore. Il passera en
|
||||
* BindWidget le jour ou l'onglet Audio sera ecrit -- en attendant, il se
|
||||
* grise tout seul si tu le poses.
|
||||
*/
|
||||
UPROPERTY(meta = (BindWidgetOptional))
|
||||
TObjectPtr<UButton> AudioTabButton;
|
||||
|
||||
/**
|
||||
* Boite de confirmation POSEE DANS le WBP_Settings, et non ajoutee au
|
||||
* viewport : elle se retrouve ainsi naturellement au-dessus de l'ecran de
|
||||
* reglages, sans avoir a lui trouver un ZOrder qui n'entre en conflit ni
|
||||
* avec le menu de pause ni avec la boite d'abandon de partie.
|
||||
*
|
||||
* Absente, "Reinitialiser" agit sans rien demander.
|
||||
* Ton WBP_ConfirmDialog, a renseigner dans les Class Defaults de
|
||||
* WBP_Settings. Laisse-le vide et "Reinitialiser" agit sans rien demander.
|
||||
*/
|
||||
UPROPERTY(meta = (BindWidgetOptional))
|
||||
TObjectPtr<UConfirmDialogWidget> ResetDialog;
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Reglages")
|
||||
TSubclassOf<UConfirmDialogWidget> ResetDialogClass;
|
||||
|
||||
/** La question posee avant de reinitialiser l'onglet visible. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Reglages")
|
||||
FText ResetDialogMessage;
|
||||
|
||||
/** Ton WBP_KeyCapture. Sans lui, l'onglet Touches ne peut rien remapper. */
|
||||
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Reglages")
|
||||
TSubclassOf<UKeyCaptureWidget> KeyCaptureClass;
|
||||
|
||||
UPROPERTY(Transient, BlueprintReadOnly, Category = "Reglages")
|
||||
TObjectPtr<UConfirmDialogWidget> ResetDialog;
|
||||
|
||||
UPROPERTY(Transient, BlueprintReadOnly, Category = "Reglages")
|
||||
TObjectPtr<UKeyCaptureWidget> KeyCapture;
|
||||
|
||||
private:
|
||||
UFUNCTION()
|
||||
void HandleBackClicked();
|
||||
@@ -104,11 +179,21 @@ private:
|
||||
UFUNCTION()
|
||||
void HandleResetClicked();
|
||||
|
||||
/** Reponse a la question du bouton Reinitialiser. */
|
||||
UFUNCTION()
|
||||
void HandleResetConfirmed();
|
||||
void HandleResetAnswered(bool bConfirmed);
|
||||
|
||||
UFUNCTION()
|
||||
void HandleResetCancelled();
|
||||
void HandleDialogConfirmed();
|
||||
|
||||
UFUNCTION()
|
||||
void HandleDialogCancelled();
|
||||
|
||||
/** Referme la boite et rend la reponse a qui a pose la question. */
|
||||
void FinishConfirmation(bool bConfirmed);
|
||||
|
||||
/** Cree la boite au premier besoin. Renvoie nullptr si aucune classe n'est assignee. */
|
||||
UConfirmDialogWidget* EnsureDialog();
|
||||
|
||||
UFUNCTION()
|
||||
void HandleGameplayTabClicked();
|
||||
@@ -119,9 +204,24 @@ private:
|
||||
UFUNCTION()
|
||||
void HandleGraphicsTabClicked();
|
||||
|
||||
UFUNCTION()
|
||||
void HandleControlsTabClicked();
|
||||
|
||||
UFUNCTION()
|
||||
void HandleAudioTabClicked();
|
||||
|
||||
UFUNCTION()
|
||||
void HandleKeyCaptured(FKey CapturedKey);
|
||||
|
||||
UFUNCTION()
|
||||
void HandleKeyCaptureCancelled();
|
||||
|
||||
UFUNCTION()
|
||||
void HandleKeyConflictAnswered(bool bConfirmed);
|
||||
|
||||
/** A qui rendre la reponse du conflit affiche dans le voile. */
|
||||
FOnSettingsConfirmationResult PendingConflictAnswer;
|
||||
|
||||
/** Change de page et rafraichit la nouvelle. Ignore un index inexistant. */
|
||||
void ShowTab(int32 Index);
|
||||
|
||||
@@ -131,5 +231,16 @@ private:
|
||||
/** Grise les boutons dont la page n'existe pas encore. */
|
||||
void UpdateTabButtonStates();
|
||||
|
||||
/** A qui rendre la reponse de la boite ouverte. Vide quand rien n'est pose. */
|
||||
FOnSettingsConfirmationResult PendingConfirmation;
|
||||
|
||||
/** A qui rendre la touche capturee. Vide quand le voile est ferme. */
|
||||
FOnSettingsKeyCaptureResult PendingKeyCapture;
|
||||
|
||||
bool bResetDialogOpen = false;
|
||||
|
||||
bool bKeyCaptureOpen = false;
|
||||
|
||||
/** Valeur de repli si l'appelant oublie SetDialogZOrder : devant le fondu exclu. */
|
||||
int32 DialogZOrder = 500;
|
||||
};
|
||||
|
||||
@@ -6,12 +6,14 @@
|
||||
#include "Blueprint/UserWidget.h"
|
||||
#include "SettingsTabWidget.generated.h"
|
||||
|
||||
class USettingsMenuWidget;
|
||||
|
||||
/**
|
||||
* Base commune a tous les onglets de reglages.
|
||||
*
|
||||
* Deux verbes, rien de plus : l'ecran parent rafraichit ou reinitialise l'onglet
|
||||
* visible sans jamais savoir ce qu'il contient. Ajouter l'onglet Graphismes plus
|
||||
* tard ne demandera donc aucune ligne dans USettingsMenuWidget.
|
||||
* visible sans jamais savoir ce qu'il contient. Ajouter un onglet ne demande
|
||||
* donc aucune ligne dans USettingsMenuWidget.
|
||||
*/
|
||||
UCLASS(Abstract)
|
||||
class SURVIVAL_PROJET_API USettingsTabWidget : public UUserWidget
|
||||
@@ -24,4 +26,22 @@ public:
|
||||
|
||||
/** Remet a l'usine les seuls reglages de cet onglet, puis se rafraichit. */
|
||||
virtual void ResetToDefaults() {}
|
||||
|
||||
/**
|
||||
* L'ecran se presente a ses onglets a la construction.
|
||||
*
|
||||
* Pointeur explicite plutot qu'un GetTypedOuter au moment de s'en servir :
|
||||
* un onglet doit pouvoir demander une confirmation, et faire dependre ca de
|
||||
* la forme de l'arbre de widgets casserait le jour ou on l'imbrique
|
||||
* autrement.
|
||||
*
|
||||
* Defini dans le .cpp et non inline : TWeakObjectPtr exige un type complet
|
||||
* pour s'assigner, et inclure l'en-tete de l'ecran ici ferait dependre tous
|
||||
* les onglets de lui.
|
||||
*/
|
||||
void SetOwningMenu(USettingsMenuWidget* InMenu);
|
||||
|
||||
protected:
|
||||
UPROPERTY(Transient, BlueprintReadOnly, Category = "Reglages")
|
||||
TWeakObjectPtr<USettingsMenuWidget> OwningMenu;
|
||||
};
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
#include "Engine/GameInstance.h"
|
||||
#include "SurvivalGameInstance.generated.h"
|
||||
|
||||
class UInputMappingContext;
|
||||
class ULocalPlayer;
|
||||
|
||||
/**
|
||||
* Le seul objet qui survit aux changements de map.
|
||||
*
|
||||
@@ -33,6 +36,32 @@ public:
|
||||
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();
|
||||
|
||||
protected:
|
||||
/**
|
||||
* 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<TObjectPtr<UInputMappingContext>> MappableContexts;
|
||||
|
||||
private:
|
||||
/**
|
||||
* Stocke un NOM et pas un index : les index changent des qu'on ajoute ou
|
||||
|
||||
@@ -18,17 +18,52 @@ DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnSurvivalSettingsApplied);
|
||||
*/
|
||||
namespace SurvivalSettingsRange
|
||||
{
|
||||
// Deux granularites par reglage :
|
||||
// Step = le pas FIN, celui du glissement au curseur. C'est aussi
|
||||
// celui sur lequel les setters arrondissent : le stocker plus
|
||||
// grossier annulerait le reglage fin juste apres.
|
||||
// ArrowStep = le bond des fleches, pour traverser la plage vite.
|
||||
constexpr float SensitivityMin = 5.f;
|
||||
constexpr float SensitivityMax = 100.f;
|
||||
constexpr float SensitivityStep = 5.f;
|
||||
constexpr float SensitivityStep = 1.f;
|
||||
constexpr float SensitivityArrowStep = 5.f;
|
||||
|
||||
constexpr float FieldOfViewMin = 70.f;
|
||||
constexpr float FieldOfViewMax = 120.f;
|
||||
constexpr float FieldOfViewStep = 5.f;
|
||||
constexpr float FieldOfViewStep = 1.f;
|
||||
constexpr float FieldOfViewArrowStep = 5.f;
|
||||
|
||||
constexpr float CameraShakeMin = 0.f;
|
||||
constexpr float CameraShakeMax = 100.f;
|
||||
constexpr float CameraShakeStep = 10.f;
|
||||
constexpr float CameraShakeStep = 1.f;
|
||||
constexpr float CameraShakeArrowStep = 10.f;
|
||||
|
||||
/**
|
||||
* Luminosite en POURCENTAGE et non en gamma.
|
||||
*
|
||||
* Le gamma utile va de 0.8 a 1.4 ; affiche tel quel il tiendrait dans un
|
||||
* intervalle de 0.6 que la ligne, qui n'affiche que des entiers, arrondirait
|
||||
* a "1" partout. On stocke donc 80 a 140 et on divise a l'application.
|
||||
*/
|
||||
constexpr float BrightnessMin = 80.f;
|
||||
constexpr float BrightnessMax = 140.f;
|
||||
constexpr float BrightnessStep = 1.f;
|
||||
constexpr float BrightnessArrowStep = 10.f;
|
||||
|
||||
/**
|
||||
* Gamma d'affichage a 100 % de luminosite.
|
||||
*
|
||||
* C'est la valeur du moteur (BaseEngine.ini), PAS un neutre a 1.0 :
|
||||
* GEngine->DisplayGamma est un exposant, pas un multiplicateur. Y ecrire 1.0
|
||||
* assombrit tout l'ecran -- et comme GEngine est global, l'editeur avec.
|
||||
*/
|
||||
constexpr float BaseDisplayGamma = 2.2f;
|
||||
|
||||
/** Echelle de resolution, en pourcentage de la resolution native. */
|
||||
constexpr float ResolutionScaleMin = 50.f;
|
||||
constexpr float ResolutionScaleMax = 100.f;
|
||||
constexpr float ResolutionScaleStep = 1.f;
|
||||
constexpr float ResolutionScaleArrowStep = 10.f;
|
||||
|
||||
/**
|
||||
* Multiplicateurs reels correspondant aux extremites de l'echelle affichee.
|
||||
@@ -169,6 +204,17 @@ public:
|
||||
UFUNCTION(BlueprintCallable, Category = "Reglages|Controles")
|
||||
void SetCrouchToggle(bool bValue);
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Reglages|Affichage")
|
||||
float GetBrightness() const { return Brightness; }
|
||||
|
||||
/** Ne fait qu'enregistrer : c'est ApplySurvivalSettings qui pousse le gamma. */
|
||||
UFUNCTION(BlueprintCallable, Category = "Reglages|Affichage")
|
||||
void SetBrightness(float Value);
|
||||
|
||||
/** Remet a l'usine les seuls reglages de l'onglet Affichage qui nous appartiennent. */
|
||||
UFUNCTION(BlueprintCallable, Category = "Reglages|Affichage")
|
||||
void SetSurvivalDisplayToDefaults();
|
||||
|
||||
private:
|
||||
/** Echelle lisible (5 a 100), convertie a la lecture. Jamais brute. */
|
||||
UPROPERTY(config)
|
||||
@@ -195,4 +241,8 @@ private:
|
||||
|
||||
UPROPERTY(config)
|
||||
bool bCrouchToggle;
|
||||
|
||||
/** Pourcentage, converti en gamma a l'application. */
|
||||
UPROPERTY(config)
|
||||
float Brightness;
|
||||
};
|
||||
|
||||
@@ -8,7 +8,10 @@ public class Survival_projet : ModuleRules
|
||||
{
|
||||
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
|
||||
|
||||
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "EnhancedInput", "UMG" });
|
||||
// GameplayTags : Enhanced Input rend ses erreurs de remappage sous forme
|
||||
// de FGameplayTagContainer. Sans ce module on peut declarer le type mais
|
||||
// pas l'inspecter, et on perdrait la raison d'un echec dans le log.
|
||||
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "EnhancedInput", "GameplayTags", "UMG" });
|
||||
|
||||
PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" });
|
||||
|
||||
|
||||
Reference in New Issue
Block a user