(Feat) Add Main Menu + Music Manger
This commit is contained in:
+42
-13
@@ -4,22 +4,49 @@ Jeu de survie solo à la première personne, gameplay écrit en **C++**, Bluepri
|
|||||||
câblage d'assets. Inspirations assumées : Raft, Aloft, Valheim, esthétique solarpunk.
|
câblage d'assets. Inspirations assumées : Raft, Aloft, Valheim, esthétique solarpunk.
|
||||||
Assets d'environnement : pack **SoStylized** (sa végétation n'est pas à l'échelle humaine).
|
Assets d'environnement : pack **SoStylized** (sa végétation n'est pas à l'échelle humaine).
|
||||||
|
|
||||||
## Interlocuteur
|
## Rôle
|
||||||
|
|
||||||
L'auteur vient de **Unity** et découvre Unreal. Il écrit en français — **répondre en français**.
|
Tu es un **développeur Unreal Engine senior**. Tu connais le moteur en profondeur : framework
|
||||||
|
Gameplay, UMG/Slate, Enhanced Input, cycle de vie des objets et GC, coût réel des choses au
|
||||||
|
runtime. L'auteur écrit en français — **répondre en français**.
|
||||||
|
|
||||||
Faire systématiquement le parallèle Unity quand un concept nouveau apparaît
|
Le code que tu produis est du code de production, pas du prototype :
|
||||||
(Blueprint ≈ prefab, DataAsset ≈ ScriptableObject, ActorComponent ≈ MonoBehaviour,
|
|
||||||
BeginPlay ≈ Start, Trace Channel ≈ LayerMask). Il raisonne déjà très bien en composants,
|
|
||||||
inutile de réexpliquer les bases.
|
|
||||||
|
|
||||||
Signaler en revanche les endroits où le modèle **diverge réellement** de Unity, ce sont ceux
|
- **Propre** : responsabilités séparées, pas de logique dupliquée, pas de valeur en dur qui
|
||||||
qui le piègent : framework GameMode/Controller/Pawn imposé, `UPROPERTY` obligatoire pour le GC,
|
devrait être exposée, noms explicites, `const` et références là où c'est correct.
|
||||||
constructeur qui tourne dans l'éditeur (CDO), pas de matrice de collision globale.
|
- **Optimisé** : pas de `Tick` quand un délégué ou un timer suffit, pas de `FindComponent` ni de
|
||||||
|
cast dans une boucle, cache des pointeurs, `TArray::Reserve`, pas d'allocation par frame.
|
||||||
|
Signaler quand une approche coûte cher et proposer l'alternative.
|
||||||
|
- **Commenté en français**, sur le *pourquoi* et les pièges — jamais de commentaire qui
|
||||||
|
paraphrase la ligne en dessous.
|
||||||
|
|
||||||
Il valide les décisions de design lui-même. Proposer **une recommandation argumentée**, pas un
|
### Guider pas à pas dans l'éditeur
|
||||||
catalogue d'options. Quand un choix change matériellement le code (modèle d'inventaire, règles
|
|
||||||
de mort), demander avant de coder plutôt que de deviner.
|
C'est le point le plus important. **Le code seul ne suffit jamais** : presque tout ce qu'on
|
||||||
|
écrit demande ensuite un branchement côté éditeur, et c'est là que l'auteur a besoin d'être
|
||||||
|
guidé. Donc pour chaque tâche :
|
||||||
|
|
||||||
|
1. Écrire et compiler le C++.
|
||||||
|
2. Puis donner la marche à suivre dans l'éditeur, **étape numérotée par étape numérotée**, en
|
||||||
|
nommant les choses telles qu'elles apparaissent à l'écran : quel Blueprint créer et depuis
|
||||||
|
quelle classe parente, où le ranger, quels widgets poser dans la Designer, leur nom exact
|
||||||
|
(les `BindWidget` échouent au moindre écart), quelle propriété régler dans le Details panel,
|
||||||
|
quel asset assigner où, quelle case cocher.
|
||||||
|
3. Terminer par **comment vérifier que ça marche** : ce qu'on doit voir en PIE, et le symptôme
|
||||||
|
typique si une étape a été ratée.
|
||||||
|
|
||||||
|
Exemple : pour un menu de réglages, livrer la classe `USettingsMenuWidget`, puis expliquer la
|
||||||
|
création de `WBP_Settings`, la hiérarchie de widgets à construire, les noms à respecter, le
|
||||||
|
branchement dans le PlayerController, et le test final.
|
||||||
|
|
||||||
|
Ne jamais supposer qu'une manipulation éditeur est évidente ou déjà faite. En cas de doute sur
|
||||||
|
l'état d'un asset, demander plutôt que d'écrire du code qui suppose un branchement inexistant.
|
||||||
|
|
||||||
|
### Décisions
|
||||||
|
|
||||||
|
L'auteur valide les décisions de design lui-même. Proposer **une recommandation argumentée**,
|
||||||
|
pas un catalogue d'options. Quand un choix change matériellement le code (modèle d'inventaire,
|
||||||
|
règles de mort), demander avant de coder plutôt que de deviner.
|
||||||
|
|
||||||
## Compiler
|
## Compiler
|
||||||
|
|
||||||
@@ -123,7 +150,9 @@ qu'une compilation propre l'est vraiment.
|
|||||||
- **Réglages** : `USurvivalUserSettings` fait autorité, jamais le CDO du pawn. Application
|
- **Réglages** : `USurvivalUserSettings` fait autorité, jamais le CDO du pawn. Application
|
||||||
immédiate, aucun bouton « Appliquer » ; la sauvegarde a lieu à la fermeture de l'écran.
|
immédiate, aucun bouton « Appliquer » ; la sauvegarde a lieu à la fermeture de l'écran.
|
||||||
L'écran ignore qui l'ouvre (`OnCloseRequested`), d'où un seul `WBP_Settings` pour le menu
|
L'écran ignore qui l'ouvre (`OnCloseRequested`), d'où un seul `WBP_Settings` pour le menu
|
||||||
principal et la pause. Une ligne = un index dans une liste de paliers, y compris les nombres :
|
principal et la pause — et il reçoit même le ZOrder de sa boîte de dialogue par
|
||||||
|
`SetDialogZOrder()` : **le contrôleur reste seul maître de son empilement**, un widget
|
||||||
|
réutilisable n'a pas à connaître la pile de son hôte. Une ligne = un index dans une liste de paliers, y compris les nombres :
|
||||||
d'où un seul `WBP_SettingRow` pour tout le menu.
|
d'où un seul `WBP_SettingRow` pour tout le menu.
|
||||||
|
|
||||||
## Conventions
|
## Conventions
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
GameDefaultMap=/Game/Game/Maps/MenuScene.MenuScene
|
GameDefaultMap=/Game/Game/Maps/MenuScene.MenuScene
|
||||||
GlobalDefaultGameMode=/Game/Game/GameMode/GM_FpsPlayer.GM_FpsPlayer_C
|
GlobalDefaultGameMode=/Game/Game/GameMode/GM_FpsPlayer.GM_FpsPlayer_C
|
||||||
EditorStartupMap=/Game/Game/Maps/GameScene.GameScene
|
EditorStartupMap=/Game/Game/Maps/GameScene.GameScene
|
||||||
|
GameInstanceClass=/Game/Game/Game-Instance/BP_SurvivalGameInstance.BP_SurvivalGameInstance_C
|
||||||
|
|
||||||
[/Script/Engine.RendererSettings]
|
[/Script/Engine.RendererSettings]
|
||||||
r.AllowStaticLighting=False
|
r.AllowStaticLighting=False
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -33,6 +33,9 @@ void UConfirmDialogWidget::NativeConstruct()
|
|||||||
|
|
||||||
void UConfirmDialogWidget::Open(const FText& InMessage)
|
void UConfirmDialogWidget::Open(const FText& InMessage)
|
||||||
{
|
{
|
||||||
|
// Une ouverture simple annule un rebours laisse par la precedente.
|
||||||
|
StopCountdown();
|
||||||
|
|
||||||
if (MessageText)
|
if (MessageText)
|
||||||
{
|
{
|
||||||
MessageText->SetText(InMessage);
|
MessageText->SetText(InMessage);
|
||||||
@@ -45,11 +48,79 @@ void UConfirmDialogWidget::Open(const FText& InMessage)
|
|||||||
SetKeyboardFocus();
|
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()
|
void UConfirmDialogWidget::Close()
|
||||||
{
|
{
|
||||||
|
StopCountdown();
|
||||||
|
|
||||||
SetVisibility(ESlateVisibility::Collapsed);
|
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)
|
FReply UConfirmDialogWidget::NativeOnKeyDown(const FGeometry& InGeometry, const FKeyEvent& InKeyEvent)
|
||||||
{
|
{
|
||||||
const FKey Key = InKeyEvent.GetKey();
|
const FKey Key = InKeyEvent.GetKey();
|
||||||
@@ -64,6 +135,7 @@ FReply UConfirmDialogWidget::NativeOnKeyDown(const FGeometry& InGeometry, const
|
|||||||
// reellement branchee.
|
// reellement branchee.
|
||||||
if (bCancelOnEscape && (Key == EKeys::Escape || Key == EKeys::Virtual_Gamepad_Back.GetVirtualKey()))
|
if (bCancelOnEscape && (Key == EKeys::Escape || Key == EKeys::Virtual_Gamepad_Back.GetVirtualKey()))
|
||||||
{
|
{
|
||||||
|
StopCountdown();
|
||||||
OnCancelled.Broadcast();
|
OnCancelled.Broadcast();
|
||||||
|
|
||||||
// Handled et non Unhandled : sans ca la touche continue de remonter et
|
// 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()))
|
if (bConfirmOnEnter && (Key == EKeys::Enter || Key == EKeys::Virtual_Gamepad_Accept.GetVirtualKey()))
|
||||||
{
|
{
|
||||||
|
StopCountdown();
|
||||||
OnConfirmed.Broadcast();
|
OnConfirmed.Broadcast();
|
||||||
return FReply::Handled();
|
return FReply::Handled();
|
||||||
}
|
}
|
||||||
@@ -82,16 +155,26 @@ FReply UConfirmDialogWidget::NativeOnKeyDown(const FGeometry& InGeometry, const
|
|||||||
|
|
||||||
void UConfirmDialogWidget::HandleConfirmClicked()
|
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();
|
OnConfirmed.Broadcast();
|
||||||
}
|
}
|
||||||
|
|
||||||
void UConfirmDialogWidget::HandleCancelClicked()
|
void UConfirmDialogWidget::HandleCancelClicked()
|
||||||
{
|
{
|
||||||
|
StopCountdown();
|
||||||
|
|
||||||
OnCancelled.Broadcast();
|
OnCancelled.Broadcast();
|
||||||
}
|
}
|
||||||
|
|
||||||
void UConfirmDialogWidget::NativeDestruct()
|
void UConfirmDialogWidget::NativeDestruct()
|
||||||
{
|
{
|
||||||
|
// Le ticker du moteur survit au widget : sans ce retrait, il rappellerait
|
||||||
|
// une methode sur un objet detruit.
|
||||||
|
StopCountdown();
|
||||||
|
|
||||||
if (ConfirmButton)
|
if (ConfirmButton)
|
||||||
{
|
{
|
||||||
ConfirmButton->OnClicked.RemoveDynamic(this, &UConfirmDialogWidget::HandleConfirmClicked);
|
ConfirmButton->OnClicked.RemoveDynamic(this, &UConfirmDialogWidget::HandleConfirmClicked);
|
||||||
|
|||||||
@@ -97,6 +97,10 @@ void AFpsPlayer::BeginPlay()
|
|||||||
{
|
{
|
||||||
if (DefaultMappingContext)
|
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);
|
Subsystem->AddMappingContext(DefaultMappingContext, 0);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -539,6 +539,12 @@ void AFpsPlayerController::OpenSettings()
|
|||||||
|
|
||||||
SettingsMenu->OnCloseRequested.AddDynamic(this, &AFpsPlayerController::HandleSettingsCloseRequested);
|
SettingsMenu->OnCloseRequested.AddDynamic(this, &AFpsPlayerController::HandleSettingsCloseRequested);
|
||||||
SettingsMenu->AddToViewport(ZOrderInGameSettingsMenu);
|
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;
|
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->OnCloseRequested.AddDynamic(this, &AMainMenuPlayerController::HandleSettingsCloseRequested);
|
||||||
SettingsMenu->AddToViewport(ZOrderMainMenuSettings);
|
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();
|
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/Button.h"
|
||||||
#include "Components/ProgressBar.h"
|
#include "Components/ProgressBar.h"
|
||||||
|
#include "Components/Slider.h"
|
||||||
#include "Components/TextBlock.h"
|
#include "Components/TextBlock.h"
|
||||||
|
|
||||||
void USettingRowWidget::NativeOnInitialized()
|
void USettingRowWidget::NativeOnInitialized()
|
||||||
@@ -24,6 +25,20 @@ void USettingRowWidget::NativeOnInitialized()
|
|||||||
{
|
{
|
||||||
RightArrowButton->OnClicked.AddDynamic(this, &USettingRowWidget::HandleRightClicked);
|
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)
|
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;
|
bNumeric = false;
|
||||||
ChoiceLabels = Options;
|
ChoiceLabels = Options;
|
||||||
StepCount = FMath::Max(Options.Num(), 1);
|
StepCount = FMath::Max(Options.Num(), 1);
|
||||||
|
ArrowIndexDelta = 1;
|
||||||
|
MaxSelectableIndex = INDEX_NONE;
|
||||||
CurrentIndex = FMath::Clamp(Index, 0, StepCount - 1);
|
CurrentIndex = FMath::Clamp(Index, 0, StepCount - 1);
|
||||||
|
|
||||||
if (LabelText)
|
if (LabelText)
|
||||||
@@ -38,18 +55,22 @@ void USettingRowWidget::SetupAsChoice(const FText& Label, const TArray<FText>& O
|
|||||||
LabelText->SetText(Label);
|
LabelText->SetText(Label);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Une barre de remplissage sur "Maintien / Bascule" ne veut rien dire : elle
|
// Une jauge sur "Maintien / Bascule" ne veut rien dire : deux positions et
|
||||||
// serait vide ou pleine sans que ca informe de quoi que ce soit. On la
|
// rien a doser. On replie l'empilement plutot que d'imposer un second WBP.
|
||||||
// masque plutot que d'imposer un second WBP juste pour les choix.
|
|
||||||
if (ValueBar)
|
if (ValueBar)
|
||||||
{
|
{
|
||||||
ValueBar->SetVisibility(ESlateVisibility::Collapsed);
|
ValueBar->SetVisibility(ESlateVisibility::Collapsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ValueSlider)
|
||||||
|
{
|
||||||
|
ValueSlider->SetVisibility(ESlateVisibility::Collapsed);
|
||||||
|
}
|
||||||
|
|
||||||
Refresh();
|
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;
|
bNumeric = true;
|
||||||
ChoiceLabels.Reset();
|
ChoiceLabels.Reset();
|
||||||
@@ -57,21 +78,39 @@ void USettingRowWidget::SetupAsNumber(const FText& Label, float Min, float Max,
|
|||||||
NumericStep = FMath::Max(Step, KINDA_SMALL_NUMBER);
|
NumericStep = FMath::Max(Step, KINDA_SMALL_NUMBER);
|
||||||
NumericSuffix = Suffix;
|
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);
|
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)
|
if (LabelText)
|
||||||
{
|
{
|
||||||
LabelText->SetText(Label);
|
LabelText->SetText(Label);
|
||||||
}
|
}
|
||||||
|
|
||||||
// HitTestInvisible et non Visible : la barre est un decor, elle ne doit pas
|
// HitTestInvisible : le remplissage est un decor pose SOUS le curseur, il ne
|
||||||
// intercepter les clics destines aux fleches qui l'encadrent.
|
// doit surtout pas intercepter le clic qui devrait lancer le glissement.
|
||||||
if (ValueBar)
|
if (ValueBar)
|
||||||
{
|
{
|
||||||
ValueBar->SetVisibility(ESlateVisibility::HitTestInvisible);
|
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);
|
SetNumericValueSilently(Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,10 +121,25 @@ float USettingRowWidget::GetNumericValue() const
|
|||||||
|
|
||||||
void USettingRowWidget::SetIndexSilently(int32 NewIndex)
|
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);
|
CurrentIndex = FMath::Clamp(NewIndex, 0, StepCount - 1);
|
||||||
Refresh();
|
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)
|
void USettingRowWidget::SetNumericValueSilently(float Value)
|
||||||
{
|
{
|
||||||
SetIndexSilently(FMath::RoundToInt((Value - NumericMin) / NumericStep));
|
SetIndexSilently(FMath::RoundToInt((Value - NumericMin) / NumericStep));
|
||||||
@@ -93,17 +147,57 @@ void USettingRowWidget::SetNumericValueSilently(float Value)
|
|||||||
|
|
||||||
void USettingRowWidget::HandleLeftClicked()
|
void USettingRowWidget::HandleLeftClicked()
|
||||||
{
|
{
|
||||||
StepIndex(-1);
|
StepIndex(ArrowTargetIndex(-1) - CurrentIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
void USettingRowWidget::HandleRightClicked()
|
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)
|
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
|
// 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.
|
// clic de trop rendrait le jeu injouable sans qu'on comprenne pourquoi.
|
||||||
@@ -143,13 +237,22 @@ void USettingRowWidget::Refresh()
|
|||||||
|
|
||||||
if (ValueBar)
|
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.
|
// plutot que de diviser par zero.
|
||||||
ValueBar->SetPercent(StepCount > 1
|
ValueBar->SetPercent(StepCount > 1
|
||||||
? static_cast<float>(CurrentIndex) / static_cast<float>(StepCount - 1)
|
? static_cast<float>(CurrentIndex) / static_cast<float>(StepCount - 1)
|
||||||
: 1.f);
|
: 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
|
// Les fleches se grisent aux extremites : le joueur voit qu'il est au bout
|
||||||
// au lieu de cliquer dans le vide.
|
// au lieu de cliquer dans le vide.
|
||||||
if (LeftArrowButton)
|
if (LeftArrowButton)
|
||||||
@@ -159,6 +262,6 @@ void USettingRowWidget::Refresh()
|
|||||||
|
|
||||||
if (RightArrowButton)
|
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)
|
if (SensitivityRow)
|
||||||
{
|
{
|
||||||
SensitivityRow->SetupAsNumber(LOCTEXT("Sensibilite", "Sensibilité"),
|
SensitivityRow->SetupAsNumber(LOCTEXT("Sensibilite", "Sensibilité"),
|
||||||
SensitivityMin, SensitivityMax, SensitivityStep, SensitivityMin, FText::GetEmpty());
|
SensitivityMin, SensitivityMax, SensitivityStep, SensitivityArrowStep, SensitivityMin, FText::GetEmpty());
|
||||||
SensitivityRow->OnIndexChanged.AddDynamic(this, &USettingsGameplayWidget::HandleSensitivityChanged);
|
SensitivityRow->OnIndexChanged.AddDynamic(this, &USettingsGameplayWidget::HandleSensitivityChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@ void USettingsGameplayWidget::NativeConstruct()
|
|||||||
if (SensitivityVerticalRow)
|
if (SensitivityVerticalRow)
|
||||||
{
|
{
|
||||||
SensitivityVerticalRow->SetupAsNumber(LOCTEXT("SensibiliteVerticale", "Sensibilité verticale"),
|
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);
|
SensitivityVerticalRow->OnIndexChanged.AddDynamic(this, &USettingsGameplayWidget::HandleSensitivityVerticalChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,14 +60,14 @@ void USettingsGameplayWidget::NativeConstruct()
|
|||||||
if (FieldOfViewRow)
|
if (FieldOfViewRow)
|
||||||
{
|
{
|
||||||
FieldOfViewRow->SetupAsNumber(LOCTEXT("ChampDeVision", "Champ de vision"),
|
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);
|
FieldOfViewRow->OnIndexChanged.AddDynamic(this, &USettingsGameplayWidget::HandleFieldOfViewChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (CameraShakeRow)
|
if (CameraShakeRow)
|
||||||
{
|
{
|
||||||
CameraShakeRow->SetupAsNumber(LOCTEXT("BougeCamera", "Bougé de caméra"),
|
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);
|
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/Button.h"
|
||||||
#include "Components/WidgetSwitcher.h"
|
#include "Components/WidgetSwitcher.h"
|
||||||
#include "ConfirmDialogWidget.h"
|
#include "ConfirmDialogWidget.h"
|
||||||
|
#include "EnhancedInputSubsystems.h"
|
||||||
|
#include "Engine/LocalPlayer.h"
|
||||||
#include "InputCoreTypes.h"
|
#include "InputCoreTypes.h"
|
||||||
|
#include "KeyCaptureWidget.h"
|
||||||
|
#include "UserSettings/EnhancedInputUserSettings.h"
|
||||||
#include "SettingsTabWidget.h"
|
#include "SettingsTabWidget.h"
|
||||||
#include "SurvivalUserSettings.h"
|
#include "SurvivalUserSettings.h"
|
||||||
|
|
||||||
@@ -19,7 +23,8 @@ namespace
|
|||||||
constexpr int32 TabIndexGameplay = 0;
|
constexpr int32 TabIndexGameplay = 0;
|
||||||
constexpr int32 TabIndexDisplay = 1;
|
constexpr int32 TabIndexDisplay = 1;
|
||||||
constexpr int32 TabIndexGraphics = 2;
|
constexpr int32 TabIndexGraphics = 2;
|
||||||
constexpr int32 TabIndexAudio = 3;
|
constexpr int32 TabIndexControls = 3;
|
||||||
|
constexpr int32 TabIndexAudio = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
USettingsMenuWidget::USettingsMenuWidget(const FObjectInitializer& ObjectInitializer)
|
USettingsMenuWidget::USettingsMenuWidget(const FObjectInitializer& ObjectInitializer)
|
||||||
@@ -62,16 +67,27 @@ void USettingsMenuWidget::NativeConstruct()
|
|||||||
GraphicsTabButton->OnClicked.AddDynamic(this, &USettingsMenuWidget::HandleGraphicsTabClicked);
|
GraphicsTabButton->OnClicked.AddDynamic(this, &USettingsMenuWidget::HandleGraphicsTabClicked);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ControlsTabButton)
|
||||||
|
{
|
||||||
|
ControlsTabButton->OnClicked.AddDynamic(this, &USettingsMenuWidget::HandleControlsTabClicked);
|
||||||
|
}
|
||||||
|
|
||||||
if (AudioTabButton)
|
if (AudioTabButton)
|
||||||
{
|
{
|
||||||
AudioTabButton->OnClicked.AddDynamic(this, &USettingsMenuWidget::HandleAudioTabClicked);
|
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);
|
for (int32 Index = 0; Index < TabSwitcher->GetNumWidgets(); ++Index)
|
||||||
ResetDialog->OnCancelled.AddDynamic(this, &USettingsMenuWidget::HandleResetCancelled);
|
{
|
||||||
ResetDialog->Close();
|
if (USettingsTabWidget* Tab = Cast<USettingsTabWidget>(TabSwitcher->GetWidgetAtIndex(Index)))
|
||||||
|
{
|
||||||
|
Tab->SetOwningMenu(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
UpdateTabButtonStates();
|
UpdateTabButtonStates();
|
||||||
@@ -89,6 +105,20 @@ void USettingsMenuWidget::NativeDestruct()
|
|||||||
ResetButton->OnClicked.RemoveDynamic(this, &USettingsMenuWidget::HandleResetClicked);
|
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();
|
Super::NativeDestruct();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,6 +126,9 @@ void USettingsMenuWidget::Open()
|
|||||||
{
|
{
|
||||||
SetVisibility(ESlateVisibility::Visible);
|
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
|
// 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.
|
// ailleurs : on relit toujours plutot que de faire confiance a l'affichage.
|
||||||
if (USettingsTabWidget* ActiveTab = GetActiveTab())
|
if (USettingsTabWidget* ActiveTab = GetActiveTab())
|
||||||
@@ -109,12 +142,18 @@ void USettingsMenuWidget::Open()
|
|||||||
|
|
||||||
void USettingsMenuWidget::Close()
|
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);
|
SetVisibility(ESlateVisibility::Collapsed);
|
||||||
|
|
||||||
@@ -124,6 +163,20 @@ void USettingsMenuWidget::Close()
|
|||||||
{
|
{
|
||||||
Settings->SaveSettings();
|
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)
|
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)
|
if (Key == EKeys::Escape)
|
||||||
{
|
{
|
||||||
// La boite de confirmation gere Echap elle-meme quand elle a le focus.
|
// La boite et le voile gerent Echap eux-memes quand ils ont le focus.
|
||||||
// Si elle l'a perdu, on annule ici plutot que de fermer tout l'ecran en
|
// S'ils l'ont perdu, on annule ici plutot que de fermer tout l'ecran en
|
||||||
// laissant la question affichee.
|
// laissant la question ou le voile affiche.
|
||||||
|
if (bKeyCaptureOpen)
|
||||||
|
{
|
||||||
|
CloseKeyCapture();
|
||||||
|
return FReply::Handled();
|
||||||
|
}
|
||||||
|
|
||||||
if (bResetDialogOpen)
|
if (bResetDialogOpen)
|
||||||
{
|
{
|
||||||
HandleResetCancelled();
|
FinishConfirmation(false);
|
||||||
return FReply::Handled();
|
return FReply::Handled();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,6 +224,9 @@ void USettingsMenuWidget::ShowTab(int32 Index)
|
|||||||
|
|
||||||
TabSwitcher->SetActiveWidgetIndex(Index);
|
TabSwitcher->SetActiveWidgetIndex(Index);
|
||||||
|
|
||||||
|
// L'onglet a change, donc le bouton a griser aussi.
|
||||||
|
UpdateTabButtonStates();
|
||||||
|
|
||||||
if (USettingsTabWidget* ActiveTab = GetActiveTab())
|
if (USettingsTabWidget* ActiveTab = GetActiveTab())
|
||||||
{
|
{
|
||||||
ActiveTab->RefreshFromSettings();
|
ActiveTab->RefreshFromSettings();
|
||||||
@@ -174,26 +236,24 @@ void USettingsMenuWidget::ShowTab(int32 Index)
|
|||||||
void USettingsMenuWidget::UpdateTabButtonStates()
|
void USettingsMenuWidget::UpdateTabButtonStates()
|
||||||
{
|
{
|
||||||
const int32 PageCount = TabSwitcher ? TabSwitcher->GetNumWidgets() : 0;
|
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)
|
ApplyState(GameplayTabButton, TabIndexGameplay);
|
||||||
{
|
ApplyState(DisplayTabButton, TabIndexDisplay);
|
||||||
DisplayTabButton->SetIsEnabled(PageCount > TabIndexDisplay);
|
ApplyState(GraphicsTabButton, TabIndexGraphics);
|
||||||
}
|
ApplyState(ControlsTabButton, TabIndexControls);
|
||||||
|
ApplyState(AudioTabButton, TabIndexAudio);
|
||||||
if (GraphicsTabButton)
|
|
||||||
{
|
|
||||||
GraphicsTabButton->SetIsEnabled(PageCount > TabIndexGraphics);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (AudioTabButton)
|
|
||||||
{
|
|
||||||
AudioTabButton->SetIsEnabled(PageCount > TabIndexAudio);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void USettingsMenuWidget::HandleBackClicked()
|
void USettingsMenuWidget::HandleBackClicked()
|
||||||
@@ -201,20 +261,66 @@ void USettingsMenuWidget::HandleBackClicked()
|
|||||||
OnCloseRequested.Broadcast();
|
OnCloseRequested.Broadcast();
|
||||||
}
|
}
|
||||||
|
|
||||||
void USettingsMenuWidget::HandleResetClicked()
|
UConfirmDialogWidget* USettingsMenuWidget::EnsureDialog()
|
||||||
{
|
{
|
||||||
// Sans boite assignee on ne bloque pas le joueur : on reinitialise.
|
if (ResetDialog)
|
||||||
|
{
|
||||||
|
return ResetDialog;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ResetDialogClass)
|
||||||
|
{
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
ResetDialog = CreateWidget<UConfirmDialogWidget>(GetOwningPlayer(), ResetDialogClass);
|
||||||
if (!ResetDialog)
|
if (!ResetDialog)
|
||||||
{
|
{
|
||||||
HandleResetConfirmed();
|
return nullptr;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ResetDialog->OnConfirmed.AddDynamic(this, &USettingsMenuWidget::HandleDialogConfirmed);
|
||||||
|
ResetDialog->OnCancelled.AddDynamic(this, &USettingsMenuWidget::HandleDialogCancelled);
|
||||||
|
ResetDialog->AddToViewport(DialogZOrder);
|
||||||
|
|
||||||
|
return ResetDialog;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
bResetDialogOpen = true;
|
||||||
ResetDialog->Open(ResetDialogMessage);
|
|
||||||
|
if (CountdownSeconds > 0.f)
|
||||||
|
{
|
||||||
|
Dialog->OpenWithCountdown(MessageOrFormat, CountdownSeconds);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Dialog->Open(MessageOrFormat);
|
||||||
}
|
}
|
||||||
|
|
||||||
void USettingsMenuWidget::HandleResetConfirmed()
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void USettingsMenuWidget::FinishConfirmation(bool bConfirmed)
|
||||||
{
|
{
|
||||||
bResetDialogOpen = false;
|
bResetDialogOpen = false;
|
||||||
|
|
||||||
@@ -223,26 +329,51 @@ void USettingsMenuWidget::HandleResetConfirmed()
|
|||||||
ResetDialog->Close();
|
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
|
// Uniquement l'onglet visible : un "Reinitialiser" qui remettrait aussi la
|
||||||
// resolution et le volume serait une mauvaise surprise.
|
// resolution et le volume serait une mauvaise surprise.
|
||||||
if (USettingsTabWidget* ActiveTab = GetActiveTab())
|
if (USettingsTabWidget* ActiveTab = GetActiveTab())
|
||||||
{
|
{
|
||||||
ActiveTab->ResetToDefaults();
|
ActiveTab->ResetToDefaults();
|
||||||
}
|
}
|
||||||
|
|
||||||
SetKeyboardFocus();
|
|
||||||
}
|
|
||||||
|
|
||||||
void USettingsMenuWidget::HandleResetCancelled()
|
|
||||||
{
|
|
||||||
bResetDialogOpen = false;
|
|
||||||
|
|
||||||
if (ResetDialog)
|
|
||||||
{
|
|
||||||
ResetDialog->Close();
|
|
||||||
}
|
|
||||||
|
|
||||||
SetKeyboardFocus();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void USettingsMenuWidget::HandleGameplayTabClicked()
|
void USettingsMenuWidget::HandleGameplayTabClicked()
|
||||||
@@ -260,9 +391,127 @@ void USettingsMenuWidget::HandleGraphicsTabClicked()
|
|||||||
ShowTab(TabIndexGraphics);
|
ShowTab(TabIndexGraphics);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void USettingsMenuWidget::HandleControlsTabClicked()
|
||||||
|
{
|
||||||
|
ShowTab(TabIndexControls);
|
||||||
|
}
|
||||||
|
|
||||||
void USettingsMenuWidget::HandleAudioTabClicked()
|
void USettingsMenuWidget::HandleAudioTabClicked()
|
||||||
{
|
{
|
||||||
ShowTab(TabIndexAudio);
|
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
|
#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 "SurvivalGameInstance.h"
|
||||||
|
|
||||||
|
#include "EnhancedInputSubsystems.h"
|
||||||
|
#include "Engine/LocalPlayer.h"
|
||||||
|
#include "InputMappingContext.h"
|
||||||
#include "SurvivalUserSettings.h"
|
#include "SurvivalUserSettings.h"
|
||||||
|
#include "UserSettings/EnhancedInputUserSettings.h"
|
||||||
|
|
||||||
void USurvivalGameInstance::Init()
|
void USurvivalGameInstance::Init()
|
||||||
{
|
{
|
||||||
Super::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
|
// 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
|
// a encore APPLIQUES : sans cet appel, la resolution et la qualite
|
||||||
// choisies au dernier lancement resteraient lettre morte jusqu'au premier
|
// 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."));
|
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
|
// valeurs sont donc bien les defauts d'usine, ecrasees ensuite par le
|
||||||
// fichier s'il existe.
|
// fichier s'il existe.
|
||||||
SetSurvivalGameplayToDefaults();
|
SetSurvivalGameplayToDefaults();
|
||||||
|
SetSurvivalDisplayToDefaults();
|
||||||
}
|
}
|
||||||
|
|
||||||
USurvivalUserSettings* USurvivalUserSettings::Get()
|
USurvivalUserSettings* USurvivalUserSettings::Get()
|
||||||
@@ -40,17 +41,47 @@ void USurvivalUserSettings::SetSurvivalGameplayToDefaults()
|
|||||||
bCrouchToggle = false;
|
bCrouchToggle = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void USurvivalUserSettings::SetSurvivalDisplayToDefaults()
|
||||||
|
{
|
||||||
|
Brightness = 100.f;
|
||||||
|
}
|
||||||
|
|
||||||
void USurvivalUserSettings::SetToDefaults()
|
void USurvivalUserSettings::SetToDefaults()
|
||||||
{
|
{
|
||||||
// Le "Reinitialiser" global du moteur doit aussi remettre NOS valeurs :
|
// 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();
|
Super::SetToDefaults();
|
||||||
|
|
||||||
SetSurvivalGameplayToDefaults();
|
SetSurvivalGameplayToDefaults();
|
||||||
|
SetSurvivalDisplayToDefaults();
|
||||||
|
}
|
||||||
|
|
||||||
|
void USurvivalUserSettings::SetBrightness(float Value)
|
||||||
|
{
|
||||||
|
using namespace SurvivalSettingsRange;
|
||||||
|
Brightness = SnapToStep(Value, BrightnessMin, BrightnessMax, BrightnessStep);
|
||||||
}
|
}
|
||||||
|
|
||||||
void USurvivalUserSettings::ApplySurvivalSettings()
|
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();
|
OnSurvivalSettingsApplied.Broadcast();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
#include "CoreMinimal.h"
|
#include "CoreMinimal.h"
|
||||||
#include "Blueprint/UserWidget.h"
|
#include "Blueprint/UserWidget.h"
|
||||||
|
#include "Containers/Ticker.h"
|
||||||
#include "ConfirmDialogWidget.generated.h"
|
#include "ConfirmDialogWidget.generated.h"
|
||||||
|
|
||||||
class UButton;
|
class UButton;
|
||||||
@@ -48,6 +49,20 @@ public:
|
|||||||
UFUNCTION(BlueprintCallable, Category = "Dialogue")
|
UFUNCTION(BlueprintCallable, Category = "Dialogue")
|
||||||
void Open(const FText& InMessage);
|
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. */
|
/** Referme la boite. Ne previent personne : l'appelant sait ce qu'il fait. */
|
||||||
UFUNCTION(BlueprintCallable, Category = "Dialogue")
|
UFUNCTION(BlueprintCallable, Category = "Dialogue")
|
||||||
void Close();
|
void Close();
|
||||||
@@ -82,4 +97,27 @@ private:
|
|||||||
|
|
||||||
UFUNCTION()
|
UFUNCTION()
|
||||||
void HandleCancelClicked();
|
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 UButton;
|
||||||
class UProgressBar;
|
class UProgressBar;
|
||||||
|
class USlider;
|
||||||
class UTextBlock;
|
class UTextBlock;
|
||||||
|
|
||||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnSettingRowIndexChanged, int32, NewIndex);
|
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.
|
* 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")
|
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")
|
UFUNCTION(BlueprintPure, Category = "Reglages")
|
||||||
int32 GetIndex() const { return CurrentIndex; }
|
int32 GetIndex() const { return CurrentIndex; }
|
||||||
@@ -75,6 +83,18 @@ public:
|
|||||||
UFUNCTION(BlueprintCallable, Category = "Reglages")
|
UFUNCTION(BlueprintCallable, Category = "Reglages")
|
||||||
void SetNumericValueSilently(float Value);
|
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:
|
protected:
|
||||||
UPROPERTY(meta = (BindWidget))
|
UPROPERTY(meta = (BindWidget))
|
||||||
TObjectPtr<UTextBlock> LabelText;
|
TObjectPtr<UTextBlock> LabelText;
|
||||||
@@ -89,14 +109,29 @@ protected:
|
|||||||
TObjectPtr<UButton> RightArrowButton;
|
TObjectPtr<UButton> RightArrowButton;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Facultative : une ligne de choix (Maintien / Bascule) n'a rien a remplir.
|
* Le REMPLISSAGE visible, et rien d'autre.
|
||||||
* 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
|
* Le Slider d'Unreal ne sait pas dessiner de portion remplie : il rend une
|
||||||
* valeurs entre deux paliers que le setter refuserait ensuite.
|
* 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))
|
UPROPERTY(meta = (BindWidgetOptional))
|
||||||
TObjectPtr<UProgressBar> ValueBar;
|
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:
|
private:
|
||||||
UFUNCTION()
|
UFUNCTION()
|
||||||
void HandleLeftClicked();
|
void HandleLeftClicked();
|
||||||
@@ -104,9 +139,19 @@ private:
|
|||||||
UFUNCTION()
|
UFUNCTION()
|
||||||
void HandleRightClicked();
|
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. */
|
/** Deplace l'index de Delta paliers et diffuse, si le mouvement est possible. */
|
||||||
void StepIndex(int32 Delta);
|
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. */
|
/** Reecrit le texte, la barre et l'etat des deux fleches. */
|
||||||
void Refresh();
|
void Refresh();
|
||||||
|
|
||||||
@@ -120,8 +165,23 @@ private:
|
|||||||
|
|
||||||
int32 CurrentIndex = 0;
|
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;
|
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;
|
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 UButton;
|
||||||
class UConfirmDialogWidget;
|
class UConfirmDialogWidget;
|
||||||
|
class UKeyCaptureWidget;
|
||||||
class USettingsTabWidget;
|
class USettingsTabWidget;
|
||||||
class UWidgetSwitcher;
|
class UWidgetSwitcher;
|
||||||
|
|
||||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnSettingsCloseRequested);
|
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
|
* L'ecran de reglages : une barre d'onglets, un WidgetSwitcher, Retour et
|
||||||
* Reinitialiser.
|
* Reinitialiser.
|
||||||
@@ -55,6 +62,57 @@ public:
|
|||||||
UFUNCTION(BlueprintCallable, Category = "Reglages")
|
UFUNCTION(BlueprintCallable, Category = "Reglages")
|
||||||
void Close();
|
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:
|
protected:
|
||||||
UPROPERTY(meta = (BindWidget))
|
UPROPERTY(meta = (BindWidget))
|
||||||
TObjectPtr<UWidgetSwitcher> TabSwitcher;
|
TObjectPtr<UWidgetSwitcher> TabSwitcher;
|
||||||
@@ -62,41 +120,58 @@ protected:
|
|||||||
UPROPERTY(meta = (BindWidget))
|
UPROPERTY(meta = (BindWidget))
|
||||||
TObjectPtr<UButton> BackButton;
|
TObjectPtr<UButton> BackButton;
|
||||||
|
|
||||||
UPROPERTY(meta = (BindWidgetOptional))
|
UPROPERTY(meta = (BindWidget))
|
||||||
TObjectPtr<UButton> ResetButton;
|
TObjectPtr<UButton> ResetButton;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Les onglets a venir. Grises automatiquement tant que le WidgetSwitcher ne
|
* Les trois onglets qui existent sont OBLIGATOIRES : un WBP_Settings sans eux
|
||||||
* contient pas la page correspondante -- meme principe que les boutons de
|
* ne compile pas.
|
||||||
* coop du menu principal : ils annoncent la couleur sans mener nulle part.
|
*
|
||||||
|
* 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;
|
TObjectPtr<UButton> GameplayTabButton;
|
||||||
|
|
||||||
UPROPERTY(meta = (BindWidgetOptional))
|
UPROPERTY(meta = (BindWidget))
|
||||||
TObjectPtr<UButton> DisplayTabButton;
|
TObjectPtr<UButton> DisplayTabButton;
|
||||||
|
|
||||||
UPROPERTY(meta = (BindWidgetOptional))
|
UPROPERTY(meta = (BindWidget))
|
||||||
TObjectPtr<UButton> GraphicsTabButton;
|
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))
|
UPROPERTY(meta = (BindWidgetOptional))
|
||||||
TObjectPtr<UButton> AudioTabButton;
|
TObjectPtr<UButton> AudioTabButton;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Boite de confirmation POSEE DANS le WBP_Settings, et non ajoutee au
|
* Ton WBP_ConfirmDialog, a renseigner dans les Class Defaults de
|
||||||
* viewport : elle se retrouve ainsi naturellement au-dessus de l'ecran de
|
* WBP_Settings. Laisse-le vide et "Reinitialiser" agit sans rien demander.
|
||||||
* 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.
|
|
||||||
*/
|
*/
|
||||||
UPROPERTY(meta = (BindWidgetOptional))
|
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Reglages")
|
||||||
TObjectPtr<UConfirmDialogWidget> ResetDialog;
|
TSubclassOf<UConfirmDialogWidget> ResetDialogClass;
|
||||||
|
|
||||||
/** La question posee avant de reinitialiser l'onglet visible. */
|
/** La question posee avant de reinitialiser l'onglet visible. */
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Reglages")
|
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Reglages")
|
||||||
FText ResetDialogMessage;
|
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:
|
private:
|
||||||
UFUNCTION()
|
UFUNCTION()
|
||||||
void HandleBackClicked();
|
void HandleBackClicked();
|
||||||
@@ -104,11 +179,21 @@ private:
|
|||||||
UFUNCTION()
|
UFUNCTION()
|
||||||
void HandleResetClicked();
|
void HandleResetClicked();
|
||||||
|
|
||||||
|
/** Reponse a la question du bouton Reinitialiser. */
|
||||||
UFUNCTION()
|
UFUNCTION()
|
||||||
void HandleResetConfirmed();
|
void HandleResetAnswered(bool bConfirmed);
|
||||||
|
|
||||||
UFUNCTION()
|
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()
|
UFUNCTION()
|
||||||
void HandleGameplayTabClicked();
|
void HandleGameplayTabClicked();
|
||||||
@@ -119,9 +204,24 @@ private:
|
|||||||
UFUNCTION()
|
UFUNCTION()
|
||||||
void HandleGraphicsTabClicked();
|
void HandleGraphicsTabClicked();
|
||||||
|
|
||||||
|
UFUNCTION()
|
||||||
|
void HandleControlsTabClicked();
|
||||||
|
|
||||||
UFUNCTION()
|
UFUNCTION()
|
||||||
void HandleAudioTabClicked();
|
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. */
|
/** Change de page et rafraichit la nouvelle. Ignore un index inexistant. */
|
||||||
void ShowTab(int32 Index);
|
void ShowTab(int32 Index);
|
||||||
|
|
||||||
@@ -131,5 +231,16 @@ private:
|
|||||||
/** Grise les boutons dont la page n'existe pas encore. */
|
/** Grise les boutons dont la page n'existe pas encore. */
|
||||||
void UpdateTabButtonStates();
|
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 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 "Blueprint/UserWidget.h"
|
||||||
#include "SettingsTabWidget.generated.h"
|
#include "SettingsTabWidget.generated.h"
|
||||||
|
|
||||||
|
class USettingsMenuWidget;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base commune a tous les onglets de reglages.
|
* Base commune a tous les onglets de reglages.
|
||||||
*
|
*
|
||||||
* Deux verbes, rien de plus : l'ecran parent rafraichit ou reinitialise l'onglet
|
* 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
|
* visible sans jamais savoir ce qu'il contient. Ajouter un onglet ne demande
|
||||||
* tard ne demandera donc aucune ligne dans USettingsMenuWidget.
|
* donc aucune ligne dans USettingsMenuWidget.
|
||||||
*/
|
*/
|
||||||
UCLASS(Abstract)
|
UCLASS(Abstract)
|
||||||
class SURVIVAL_PROJET_API USettingsTabWidget : public UUserWidget
|
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. */
|
/** Remet a l'usine les seuls reglages de cet onglet, puis se rafraichit. */
|
||||||
virtual void ResetToDefaults() {}
|
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 "Engine/GameInstance.h"
|
||||||
#include "SurvivalGameInstance.generated.h"
|
#include "SurvivalGameInstance.generated.h"
|
||||||
|
|
||||||
|
class UInputMappingContext;
|
||||||
|
class ULocalPlayer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Le seul objet qui survit aux changements de map.
|
* Le seul objet qui survit aux changements de map.
|
||||||
*
|
*
|
||||||
@@ -33,6 +36,32 @@ public:
|
|||||||
UFUNCTION(BlueprintCallable, Category = "Menu")
|
UFUNCTION(BlueprintCallable, Category = "Menu")
|
||||||
void SetLastMenuCameraName(FName SpotName) { LastMenuCameraName = SpotName; }
|
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:
|
private:
|
||||||
/**
|
/**
|
||||||
* Stocke un NOM et pas un index : les index changent des qu'on ajoute ou
|
* 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
|
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 SensitivityMin = 5.f;
|
||||||
constexpr float SensitivityMax = 100.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 FieldOfViewMin = 70.f;
|
||||||
constexpr float FieldOfViewMax = 120.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 CameraShakeMin = 0.f;
|
||||||
constexpr float CameraShakeMax = 100.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.
|
* Multiplicateurs reels correspondant aux extremites de l'echelle affichee.
|
||||||
@@ -169,6 +204,17 @@ public:
|
|||||||
UFUNCTION(BlueprintCallable, Category = "Reglages|Controles")
|
UFUNCTION(BlueprintCallable, Category = "Reglages|Controles")
|
||||||
void SetCrouchToggle(bool bValue);
|
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:
|
private:
|
||||||
/** Echelle lisible (5 a 100), convertie a la lecture. Jamais brute. */
|
/** Echelle lisible (5 a 100), convertie a la lecture. Jamais brute. */
|
||||||
UPROPERTY(config)
|
UPROPERTY(config)
|
||||||
@@ -195,4 +241,8 @@ private:
|
|||||||
|
|
||||||
UPROPERTY(config)
|
UPROPERTY(config)
|
||||||
bool bCrouchToggle;
|
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;
|
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" });
|
PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" });
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user