(Feat) Add Animation
This commit is contained in:
+48
-2
@@ -115,6 +115,7 @@ qu'une compilation propre l'est vraiment.
|
|||||||
| Système | Fichiers |
|
| Système | Fichiers |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Personnage, caméra, game feel | `FpsPlayer.h/.cpp` |
|
| Personnage, caméra, game feel | `FpsPlayer.h/.cpp` |
|
||||||
|
| Balancement de tête procédural | `HeadBobComponent` (profils par état, additionné dans `ApplyCameraTransform`) |
|
||||||
| HUD, input d'interface, fondus, respawn | `FpsPlayerController.h/.cpp` |
|
| HUD, input d'interface, fondus, respawn | `FpsPlayerController.h/.cpp` |
|
||||||
| Interaction (trace + interface) | `Interactable.h`, `InteractionComponent`, `InteractionPromptWidget` |
|
| Interaction (trace + interface) | `Interactable.h`, `InteractionComponent`, `InteractionPromptWidget` |
|
||||||
| Objets | `ItemDataAsset`, `PickupItem` |
|
| Objets | `ItemDataAsset`, `PickupItem` |
|
||||||
@@ -127,6 +128,8 @@ qu'une compilation propre l'est vraiment.
|
|||||||
| Écran à onglets (inventaire / craft) | `InventoryScreenWidget` |
|
| Écran à onglets (inventaire / craft) | `InventoryScreenWidget` |
|
||||||
| Menu de pause | `PauseMenuWidget` + la partie pause de `FpsPlayerController` |
|
| Menu de pause | `PauseMenuWidget` + la partie pause de `FpsPlayerController` |
|
||||||
| Réglages | `SurvivalUserSettings`, `SettingsMenuWidget`, `SettingsTabWidget`, `SettingsGameplayWidget`, `SettingRowWidget` |
|
| Réglages | `SurvivalUserSettings`, `SettingsMenuWidget`, `SettingsTabWidget`, `SettingsGameplayWidget`, `SettingRowWidget` |
|
||||||
|
| Musique | `MusicDirector`, `MusicSubsystem`, `MusicPlaylistDataAsset` |
|
||||||
|
| Audio (volumes) | `SurvivalAudioSettings`, `SettingsAudioWidget` + `USurvivalUserSettings::ApplyAudioSettings` |
|
||||||
| Fondus, boîtes de dialogue | `ScreenFadeComponent`, `ConfirmDialogWidget` |
|
| Fondus, boîtes de dialogue | `ScreenFadeComponent`, `ConfirmDialogWidget` |
|
||||||
| Coop Steam (sessions) | `SessionSubsystem`, `SurvivalGameMode`, `SurvivalGameState`, `SurvivalPlayerState` |
|
| Coop Steam (sessions) | `SessionSubsystem`, `SurvivalGameMode`, `SurvivalGameState`, `SurvivalPlayerState` |
|
||||||
| Pseudo au-dessus des joueurs | `PlayerNameplateComponent`, `PlayerNameplateWidget` |
|
| Pseudo au-dessus des joueurs | `PlayerNameplateComponent`, `PlayerNameplateWidget` |
|
||||||
@@ -384,6 +387,49 @@ jetée**. On route donc toujours par l'inventaire du pawn local, et la source co
|
|||||||
sont passées en paramètres — jamais déduites du récepteur, sinon « Tout prendre » viderait le sac
|
sont passées en paramètres — jamais déduites du récepteur, sinon « Tout prendre » viderait le sac
|
||||||
du joueur dans lui-même.
|
du joueur dans lui-même.
|
||||||
|
|
||||||
|
### Audio — le mixage passe par les SoundClass, et par rien d'autre
|
||||||
|
|
||||||
|
- **Une hiérarchie de `SoundClass`, un `SoundMix`, quatre curseurs.** `SC_Master` est la
|
||||||
|
racine ; `SC_Music`, `SC_SFX` et `SC_UI` en descendent. Le moteur multiplie déjà le volume
|
||||||
|
du parent dans celui de l'enfant, donc le volume général est gratuit — pas de produit à
|
||||||
|
calculer en C++.
|
||||||
|
- **`SM_Settings` est déclaré comme *Default Base Sound Mix*** dans Project Settings > Audio,
|
||||||
|
et le C++ le relit à cet endroit au lieu d'en garder une référence à lui. Deux raisons : un
|
||||||
|
mix de base est **rejoué à plein régime en permanence** (un mix poussé à la main s'éteindrait
|
||||||
|
au bout de sa durée), et le moteur le **repose à chaque chargement de map** — donc les
|
||||||
|
volumes traversent les transitions sans une ligne de code. Une seconde référence côté projet
|
||||||
|
n'apporterait qu'un risque de divergence.
|
||||||
|
- **`bApplyToChildren = true` sur le maître, `false` sur les trois autres.** C'est le piège de
|
||||||
|
ce système : le moteur propage les propriétés parent → enfant (`ParseSoundClasses`) **avant**
|
||||||
|
d'appliquer les ajusteurs du mix (`ApplyClassAdjusters`). Un ajusteur posé sur `SC_Master`
|
||||||
|
arrive donc trop tard pour redescendre tout seul. Sans ce drapeau, le volume général n'agit
|
||||||
|
que sur les sons rangés *directement* dans `SC_Master` — c'est-à-dire quasiment rien, et le
|
||||||
|
symptôme se lit comme « le curseur maître ne fait rien ».
|
||||||
|
- **Aucun fondu sur `SetSoundMixClassOverride` — `FadeInTime` vaut 0.** Ce n'est pas une
|
||||||
|
simplification : toute durée non nulle rend le curseur inutilisable. Reposer un override
|
||||||
|
avant la fin du précédent relance l'interpolation (`FDynamicParameter::Set` remet
|
||||||
|
`CurrTimeSec` à zéro) **sans** appeler `Update(DeltaTime)` la même frame. Un curseur qu'on
|
||||||
|
glisse repose l'override à chaque image, donc la rampe repart indéfiniment de zéro et le
|
||||||
|
volume ne rattrape son retard qu'une fois le geste **arrêté**. Le mixeur lisse déjà le gain
|
||||||
|
de chaque source dans son buffer de rendu : à zéro il n'y a pas de clic pour autant.
|
||||||
|
- **`SC_SFX` est la *Default Sound Class* du projet.** Un son qui n'annonce pas sa classe
|
||||||
|
atterrit donc dans les effets plutôt que nulle part : oublier de taguer un bruitage le laisse
|
||||||
|
quand même sous le curseur Effets. Seuls les sons d'interface se taguent à la main.
|
||||||
|
- **La musique impose sa classe depuis le code** (`SoundClassOverride` posé par
|
||||||
|
`UMusicSubsystem`), pas depuis les assets : ajouter une piste à une playlist ne doit pas
|
||||||
|
demander de penser à la taguer. Corollaire, `UMusicSubsystem::SetMusicVolume` **n'est pas** le
|
||||||
|
réglage du joueur — deux chemins vers le même volume finiraient par se multiplier.
|
||||||
|
- **Les volumes sont des pourcentages, élevés au carré avant d'être envoyés.** L'oreille répond
|
||||||
|
au logarithme de l'amplitude : un curseur envoyé tel quel donne un réglage où tout se joue
|
||||||
|
dans les vingt derniers pourcents. À 50 %, l'amplitude vaut 0,25, soit environ la moitié du
|
||||||
|
volume *perçu*.
|
||||||
|
- **`ApplyAudioSettings()` est séparée d'`ApplySurvivalSettings()`** : cinq commandes vers le
|
||||||
|
thread audio à chaque cran du champ de vision seraient du bruit pur. `ApplyNonResolutionSettings`
|
||||||
|
appelle les deux, donc le démarrage n'en oublie aucune.
|
||||||
|
- **Le jeu est muet en arrière-plan par défaut, et c'est le moteur qui le décide**
|
||||||
|
(`FApp::UnfocusedVolumeMultiplier`, dont la valeur d'usine est 0). Le réglage ne fait que
|
||||||
|
l'exposer ; il ne passe pas par le mix, puisqu'il baisse l'application entière bien en amont.
|
||||||
|
|
||||||
### Décisions de gameplay figées
|
### Décisions de gameplay figées
|
||||||
|
|
||||||
- **Inventaire** : un seul `TArray` de 30 slots. Index **0-9 = barre rapide**, **10-29 = grille**.
|
- **Inventaire** : un seul `TArray` de 30 slots. Index **0-9 = barre rapide**, **10-29 = grille**.
|
||||||
@@ -576,8 +622,8 @@ vide en dix minutes. À écrire **serveur-autoritaire d'emblée** — c'est un a
|
|||||||
|
|
||||||
Ensuite, par ordre de valeur : objet visible en main, sauvegarde, cycle jour/nuit.
|
Ensuite, par ordre de valeur : objet visible en main, sauvegarde, cycle jour/nuit.
|
||||||
|
|
||||||
> **Systèmes présents dans `Source/` mais pas encore décrits ici** : `MusicDirector`,
|
> **Systèmes présents dans `Source/` mais pas encore décrits ici** :
|
||||||
> `MusicSubsystem`, `MusicPlaylistDataAsset`, `SettingsDisplayWidget`, `SettingsGraphicsWidget`,
|
> `SettingsDisplayWidget`, `SettingsGraphicsWidget`,
|
||||||
> `SettingsControlsWidget`, `SettingKeyRowWidget`, `KeyCaptureWidget`. Ils sont arrivés en dehors
|
> `SettingsControlsWidget`, `SettingKeyRowWidget`, `KeyCaptureWidget`. Ils sont arrivés en dehors
|
||||||
> des sessions qui ont produit ce document — à documenter quand on y touchera, plutôt que de
|
> des sessions qui ont produit ce document — à documenter quand on y touchera, plutôt que de
|
||||||
> décrire de mémoire un code qu'on n'a pas lu.
|
> décrire de mémoire un code qu'on n'a pas lu.
|
||||||
|
|||||||
@@ -146,3 +146,7 @@ ConnectionType=USBOnly
|
|||||||
bUseManualIPAddress=False
|
bUseManualIPAddress=False
|
||||||
ManualIPAddress=
|
ManualIPAddress=
|
||||||
|
|
||||||
|
[/Script/Engine.AudioSettings]
|
||||||
|
DefaultSoundClassName=/Game/Game/Sounds/Mix/SC_SFX.SC_SFX
|
||||||
|
DefaultBaseSoundMix=/Game/Game/Sounds/Mix/SM_Settings.SM_Settings
|
||||||
|
|
||||||
|
|||||||
@@ -52,3 +52,9 @@ bMoviesAreSkippable=True
|
|||||||
+StartupMovies=UnrealLogo
|
+StartupMovies=UnrealLogo
|
||||||
+StartupMovies=StudioLogo
|
+StartupMovies=StudioLogo
|
||||||
|
|
||||||
|
[/Script/Survival_projet.SurvivalAudioSettings]
|
||||||
|
MasterSoundClass=/Game/Game/Sounds/Mix/SC_Master.SC_Master
|
||||||
|
MusicSoundClass=/Game/Game/Sounds/Mix/SC_Music.SC_Music
|
||||||
|
EffectsSoundClass=/Game/Game/Sounds/Mix/SC_SFX.SC_SFX
|
||||||
|
UiSoundClass=/Game/Game/Sounds/Mix/SC_UI.SC_UI
|
||||||
|
|
||||||
|
|||||||
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.
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.
@@ -0,0 +1,62 @@
|
|||||||
|
// Fill out your copyright notice in the Description page of Project Settings.
|
||||||
|
|
||||||
|
|
||||||
|
#include "FpsArmsAnimInstance.h"
|
||||||
|
|
||||||
|
#include "FpsPlayer.h"
|
||||||
|
#include "GameFramework/CharacterMovementComponent.h"
|
||||||
|
|
||||||
|
void UFpsArmsAnimInstance::NativeInitializeAnimation()
|
||||||
|
{
|
||||||
|
Super::NativeInitializeAnimation();
|
||||||
|
|
||||||
|
OwningPlayer = Cast<AFpsPlayer>(TryGetPawnOwner());
|
||||||
|
}
|
||||||
|
|
||||||
|
void UFpsArmsAnimInstance::NativeUpdateAnimation(float DeltaSeconds)
|
||||||
|
{
|
||||||
|
Super::NativeUpdateAnimation(DeltaSeconds);
|
||||||
|
|
||||||
|
// L'AnimInstance est initialisee avec le composant, c'est-a-dire potentiellement
|
||||||
|
// avant que le pawn soit completement en place. On retente donc tant qu'on n'a
|
||||||
|
// personne, plutot que d'abandonner definitivement sur un echec au demarrage.
|
||||||
|
if (!OwningPlayer.IsValid())
|
||||||
|
{
|
||||||
|
OwningPlayer = Cast<AFpsPlayer>(TryGetPawnOwner());
|
||||||
|
if (!OwningPlayer.IsValid())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const AFpsPlayer* Player = OwningPlayer.Get();
|
||||||
|
const UCharacterMovementComponent* Movement = Player->GetCharacterMovement();
|
||||||
|
if (!Movement)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size2D et pas Size : en pleine chute la composante verticale ferait courir
|
||||||
|
// les bras alors que le joueur ne pose pas un pied.
|
||||||
|
GroundSpeed = Movement->Velocity.Size2D();
|
||||||
|
|
||||||
|
// Le test d'acceleration est ce qui distingue « le joueur avance » de « le
|
||||||
|
// joueur a lache la touche et glisse encore ». Sans lui, l'animation de course
|
||||||
|
// continue pendant toute la decceleration, les pieds patinant dans le vide --
|
||||||
|
// invisible sur des bras, mais le meme AnimInstance servira au corps un jour.
|
||||||
|
bIsMoving = GroundSpeed > MovingSpeedThreshold
|
||||||
|
&& !Movement->GetCurrentAcceleration().IsNearlyZero();
|
||||||
|
|
||||||
|
bIsSprinting = Player->IsSprinting();
|
||||||
|
bIsFalling = Movement->IsFalling();
|
||||||
|
bIsCrouching = Movement->IsCrouching();
|
||||||
|
|
||||||
|
// La cadence suit la vitesse REELLE et pas l'etat de sprint : c'est ce qui
|
||||||
|
// fait que les bras accelerent pendant la montee en vitesse, au lieu de
|
||||||
|
// sauter d'une cadence a l'autre a l'instant ou la touche est pressee.
|
||||||
|
// Elle couvre donc aussi l'accroupi, gratuitement.
|
||||||
|
LocomotionPlayRate = FMath::Clamp(
|
||||||
|
GroundSpeed / ReferenceLocomotionSpeed,
|
||||||
|
MinLocomotionPlayRate,
|
||||||
|
MaxLocomotionPlayRate);
|
||||||
|
}
|
||||||
@@ -3,6 +3,8 @@
|
|||||||
|
|
||||||
#include "FpsPlayer.h"
|
#include "FpsPlayer.h"
|
||||||
|
|
||||||
|
#include "Animation/AnimInstance.h"
|
||||||
|
#include "Animation/AnimMontage.h"
|
||||||
#include "Camera/CameraComponent.h"
|
#include "Camera/CameraComponent.h"
|
||||||
#include "Camera/CameraTypes.h"
|
#include "Camera/CameraTypes.h"
|
||||||
#include "Components/CapsuleComponent.h"
|
#include "Components/CapsuleComponent.h"
|
||||||
@@ -14,11 +16,13 @@
|
|||||||
#include "GameFramework/PlayerController.h"
|
#include "GameFramework/PlayerController.h"
|
||||||
#include "InputActionValue.h"
|
#include "InputActionValue.h"
|
||||||
#include "CraftingComponent.h"
|
#include "CraftingComponent.h"
|
||||||
|
#include "HeadBobComponent.h"
|
||||||
#include "InteractionComponent.h"
|
#include "InteractionComponent.h"
|
||||||
#include "InventoryComponent.h"
|
#include "InventoryComponent.h"
|
||||||
#include "ItemDataAsset.h"
|
#include "ItemDataAsset.h"
|
||||||
#include "Net/UnrealNetwork.h"
|
#include "Net/UnrealNetwork.h"
|
||||||
#include "PickupItem.h"
|
#include "PickupItem.h"
|
||||||
|
#include "SurvivalGameInstance.h"
|
||||||
#include "SurvivalStatsComponent.h"
|
#include "SurvivalStatsComponent.h"
|
||||||
#include "SurvivalUserSettings.h"
|
#include "SurvivalUserSettings.h"
|
||||||
|
|
||||||
@@ -74,6 +78,31 @@ AFpsPlayer::AFpsPlayer()
|
|||||||
// ecraserait la rotation chaque frame et tous les effets de tilt seraient invisibles.
|
// ecraserait la rotation chaque frame et tous les effets de tilt seraient invisibles.
|
||||||
FirstPersonCamera->bUsePawnControlRotation = false;
|
FirstPersonCamera->bUsePawnControlRotation = false;
|
||||||
|
|
||||||
|
// Bras vue premiere personne, accroches a la camera pour suivre le regard.
|
||||||
|
FirstPersonArms = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("FirstPersonArms"));
|
||||||
|
FirstPersonArms->SetupAttachment(FirstPersonCamera);
|
||||||
|
// Symetrique exact du mesh 3e personne ci-dessous : chacun ne voit que ses
|
||||||
|
// propres bras. Sans ca, les bras d'un ami flotteraient devant notre objectif.
|
||||||
|
FirstPersonArms->SetOnlyOwnerSee(true);
|
||||||
|
// Un mesh colle a l'objectif projetterait une ombre enorme en travers du
|
||||||
|
// decor. L'ombre du joueur, elle, vient du mesh 3e personne.
|
||||||
|
FirstPersonArms->SetCastShadow(false);
|
||||||
|
FirstPersonArms->bCastDynamicShadow = false;
|
||||||
|
// Les bras ne sont pas dans le monde, ils sont devant l'objectif : une
|
||||||
|
// collision les ferait pousser des objets et surtout bloquer le trace
|
||||||
|
// d'interaction, qui part exactement d'entre eux.
|
||||||
|
FirstPersonArms->SetCollisionEnabled(ECollisionEnabled::NoCollision);
|
||||||
|
// Chez l'hote, le pawn de chaque ami porte AUSSI ce composant, invisible pour
|
||||||
|
// tout le monde. Sans cette option le serveur evaluerait quatre AnimBlueprints
|
||||||
|
// pour quatre meshes que personne ne rend.
|
||||||
|
FirstPersonArms->VisibilityBasedAnimTickOption = EVisibilityBasedAnimTickOption::OnlyTickPoseWhenRendered;
|
||||||
|
// Point de depart a affiner dans BP_FpsPlayer : la bonne valeur depend de
|
||||||
|
// l'origine du FBX (racine aux pieds ? au bassin ?) et de l'orientation
|
||||||
|
// d'export. Le yaw de -90 est le decalage habituel d'un FBX Blender/Maya.
|
||||||
|
FirstPersonArms->SetRelativeLocationAndRotation(FVector(0.f, 0.f, -160.f), FRotator(0.f, -90.f, 0.f));
|
||||||
|
|
||||||
|
HeadBob = CreateDefaultSubobject<UHeadBobComponent>(TEXT("HeadBob"));
|
||||||
|
|
||||||
InteractionComponent = CreateDefaultSubobject<UInteractionComponent>(TEXT("InteractionComponent"));
|
InteractionComponent = CreateDefaultSubobject<UInteractionComponent>(TEXT("InteractionComponent"));
|
||||||
InventoryComponent = CreateDefaultSubobject<UInventoryComponent>(TEXT("InventoryComponent"));
|
InventoryComponent = CreateDefaultSubobject<UInventoryComponent>(TEXT("InventoryComponent"));
|
||||||
CraftingComponent = CreateDefaultSubobject<UCraftingComponent>(TEXT("CraftingComponent"));
|
CraftingComponent = CreateDefaultSubobject<UCraftingComponent>(TEXT("CraftingComponent"));
|
||||||
@@ -131,10 +160,18 @@ void AFpsPlayer::BeginPlay()
|
|||||||
{
|
{
|
||||||
if (DefaultMappingContext)
|
if (DefaultMappingContext)
|
||||||
{
|
{
|
||||||
// On ACTIVE le contexte ici, mais on ne le DECLARE pas comme
|
// La DECLARATION des contextes remappables appartient au
|
||||||
// remappable : c'est USurvivalGameInstance qui s'en charge, parce
|
// GameInstance -- le menu principal n'a pas de pawn et doit
|
||||||
// que le menu principal n'a pas de pawn et doit quand meme
|
// pourtant afficher les touches. On la demande quand meme ici, et
|
||||||
// afficher les touches.
|
// AVANT d'activer le contexte : c'est elle qui traduit les touches
|
||||||
|
// vers la disposition clavier du joueur, et un AZERTY doit trouver
|
||||||
|
// ses touches des la premiere image, pas au premier passage dans
|
||||||
|
// l'ecran de reglages. L'appel est idempotent.
|
||||||
|
if (USurvivalGameInstance* GameInstance = GetGameInstance<USurvivalGameInstance>())
|
||||||
|
{
|
||||||
|
GameInstance->RegisterMappableContexts();
|
||||||
|
}
|
||||||
|
|
||||||
Subsystem->AddMappingContext(DefaultMappingContext, 0);
|
Subsystem->AddMappingContext(DefaultMappingContext, 0);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -176,6 +213,10 @@ void AFpsPlayer::ApplyUserSettings()
|
|||||||
bSprintToggleMode = Settings->GetSprintToggle();
|
bSprintToggleMode = Settings->GetSprintToggle();
|
||||||
bCrouchToggleMode = Settings->GetCrouchToggle();
|
bCrouchToggleMode = Settings->GetCrouchToggle();
|
||||||
|
|
||||||
|
// Le curseur "secousses de camera" vaut aussi pour le balancement : un joueur
|
||||||
|
// qui le met a zero demande une camera fixe, pas une camera fixe qui respire.
|
||||||
|
HeadBob->SetIntensityScale(CameraShakeMultiplier);
|
||||||
|
|
||||||
// Repasser en maintien alors que la touche n'est plus enfoncee laisserait le
|
// Repasser en maintien alors que la touche n'est plus enfoncee laisserait le
|
||||||
// personnage bloque en sprint : on repart d'un etat propre.
|
// personnage bloque en sprint : on repart d'un etat propre.
|
||||||
if (!bSprintToggleMode && bIsSprinting)
|
if (!bSprintToggleMode && bIsSprinting)
|
||||||
@@ -228,7 +269,15 @@ void AFpsPlayer::UpdateCameraEffects(float DeltaTime)
|
|||||||
CrouchTiltSpring.Update(Step, CrouchTiltFrequency, CrouchTiltDamping, CrouchTiltRampTime, 1.5f);
|
CrouchTiltSpring.Update(Step, CrouchTiltFrequency, CrouchTiltDamping, CrouchTiltRampTime, 1.5f);
|
||||||
|
|
||||||
// ==================================================================
|
// ==================================================================
|
||||||
// 3. FOV de sprint
|
// 3. Balancement de tete
|
||||||
|
// ==================================================================
|
||||||
|
// Integre ici et applique dans ApplyCameraTransform, exactement comme les
|
||||||
|
// ressorts ci-dessus : c'est ce qui permet a CalcCamera de recomposer la
|
||||||
|
// transform sans faire avancer le temps deux fois.
|
||||||
|
HeadBob->UpdateBob(Step);
|
||||||
|
|
||||||
|
// ==================================================================
|
||||||
|
// 4. FOV de sprint
|
||||||
// ==================================================================
|
// ==================================================================
|
||||||
// On exige un vrai deplacement : sinon le FOV s'ouvrirait juste en
|
// On exige un vrai deplacement : sinon le FOV s'ouvrirait juste en
|
||||||
// maintenant Shift a l'arret, ce qui donne un effet de pompage bizarre.
|
// maintenant Shift a l'arret, ce qui donne un effet de pompage bizarre.
|
||||||
@@ -257,7 +306,18 @@ void AFpsPlayer::ApplyCameraTransform()
|
|||||||
// demi-hauteur courante de la capsule, ce saut est annule exactement --
|
// demi-hauteur courante de la capsule, ce saut est annule exactement --
|
||||||
// a condition de lire les deux au meme instant, d'ou l'appel depuis CalcCamera.
|
// a condition de lire les deux au meme instant, d'ou l'appel depuis CalcCamera.
|
||||||
const float CapsuleHalfHeight = GetCapsuleComponent()->GetScaledCapsuleHalfHeight();
|
const float CapsuleHalfHeight = GetCapsuleComponent()->GetScaledCapsuleHalfHeight();
|
||||||
FirstPersonCamera->SetRelativeLocation(FVector(0.f, 0.f, EyeHeightAboveFeet - CapsuleHalfHeight + PunchSpring.Offset));
|
|
||||||
|
// Le balancement s'AJOUTE au reste. Son Y est un decalage lateral dans le
|
||||||
|
// repere de la capsule, dont le yaw suit deja le regard (bUseControllerRotationYaw) :
|
||||||
|
// la tete se porte donc bien a droite de ce qu'on regarde, sans conversion.
|
||||||
|
// Le tangage, lui, n'est pas dans la capsule -- c'est voulu, le balancement
|
||||||
|
// reste horizontal meme quand on regarde ses pieds.
|
||||||
|
const FVector BobOffset = HeadBob->GetLocationOffset();
|
||||||
|
|
||||||
|
FirstPersonCamera->SetRelativeLocation(FVector(
|
||||||
|
BobOffset.X,
|
||||||
|
BobOffset.Y,
|
||||||
|
EyeHeightAboveFeet - CapsuleHalfHeight + PunchSpring.Offset + BobOffset.Z));
|
||||||
|
|
||||||
// ==================================================================
|
// ==================================================================
|
||||||
// Rotation
|
// Rotation
|
||||||
@@ -274,6 +334,9 @@ void AFpsPlayer::ApplyCameraTransform()
|
|||||||
// regarde vers le bas. C'est ce qui vend l'impact a l'atterrissage.
|
// regarde vers le bas. C'est ce qui vend l'impact a l'atterrissage.
|
||||||
ViewOffset.Pitch += PunchSpring.Offset * PunchPitchPerCm;
|
ViewOffset.Pitch += PunchSpring.Offset * PunchPitchPerCm;
|
||||||
|
|
||||||
|
// Roulis et tangage du balancement, dans le meme sac que le reste.
|
||||||
|
ViewOffset += HeadBob->GetRotationOffset();
|
||||||
|
|
||||||
if (const AController* OwningController = GetController())
|
if (const AController* OwningController = GetController())
|
||||||
{
|
{
|
||||||
FirstPersonCamera->SetWorldRotation(OwningController->GetControlRotation() + ViewOffset);
|
FirstPersonCamera->SetWorldRotation(OwningController->GetControlRotation() + ViewOffset);
|
||||||
@@ -812,9 +875,35 @@ void AFpsPlayer::StopCrouch()
|
|||||||
|
|
||||||
void AFpsPlayer::Interact()
|
void AFpsPlayer::Interact()
|
||||||
{
|
{
|
||||||
|
// Le geste part AVANT la RPC, pas apres sa reponse : un montage declenche a
|
||||||
|
// l'aller-retour serveur donnerait des bras qui repondent 60 ms apres le clic,
|
||||||
|
// et le joueur lirait ca comme un jeu qui rame. Le prix a payer est un geste
|
||||||
|
// dans le vide si le serveur refuse l'interaction -- infiniment moins genant.
|
||||||
|
//
|
||||||
|
// Le test de type est ici et pas dans le composant d'interaction : celui-ci ne
|
||||||
|
// connait que l'interface IInteractable et n'a pas a apprendre ce qu'est un
|
||||||
|
// pickup. Un coffre ou un etabli n'ont pas a declencher un geste de ramassage.
|
||||||
|
if (Cast<APickupItem>(InteractionComponent->GetFocusedActor()))
|
||||||
|
{
|
||||||
|
PlayArmsMontage(GrabMontage);
|
||||||
|
}
|
||||||
|
|
||||||
InteractionComponent->TryInteract();
|
InteractionComponent->TryInteract();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
float AFpsPlayer::PlayArmsMontage(UAnimMontage* Montage, float PlayRate)
|
||||||
|
{
|
||||||
|
// IsLocallyControlled : chez l'hote, le pawn de chaque ami passerait aussi par
|
||||||
|
// ici a chaque ramassage et animerait un mesh que personne ne rend.
|
||||||
|
if (!Montage || !IsLocallyControlled())
|
||||||
|
{
|
||||||
|
return 0.f;
|
||||||
|
}
|
||||||
|
|
||||||
|
UAnimInstance* ArmsAnim = FirstPersonArms->GetAnimInstance();
|
||||||
|
return ArmsAnim ? ArmsAnim->Montage_Play(Montage, PlayRate) : 0.f;
|
||||||
|
}
|
||||||
|
|
||||||
void AFpsPlayer::OnStartCrouch(float HalfHeightAdjust, float ScaledHalfHeightAdjust)
|
void AFpsPlayer::OnStartCrouch(float HalfHeightAdjust, float ScaledHalfHeightAdjust)
|
||||||
{
|
{
|
||||||
Super::OnStartCrouch(HalfHeightAdjust, ScaledHalfHeightAdjust);
|
Super::OnStartCrouch(HalfHeightAdjust, ScaledHalfHeightAdjust);
|
||||||
|
|||||||
@@ -0,0 +1,141 @@
|
|||||||
|
// Fill out your copyright notice in the Description page of Project Settings.
|
||||||
|
|
||||||
|
|
||||||
|
#include "HeadBobComponent.h"
|
||||||
|
|
||||||
|
#include "GameFramework/Character.h"
|
||||||
|
#include "GameFramework/CharacterMovementComponent.h"
|
||||||
|
|
||||||
|
UHeadBobComponent::UHeadBobComponent()
|
||||||
|
{
|
||||||
|
// C'est le pawn qui appelle UpdateBob depuis son propre Tick, juste avant de
|
||||||
|
// composer la transform de la camera. Un tick a nous ne servirait qu'a
|
||||||
|
// introduire une incertitude d'ordre.
|
||||||
|
PrimaryComponentTick.bCanEverTick = false;
|
||||||
|
|
||||||
|
// Valeurs de depart, a affiner a l'oeil dans BP_FpsPlayer. Elles sont ici et
|
||||||
|
// non en initialiseurs de membre pour que le constructeur reste le seul
|
||||||
|
// endroit a lire quand on cherche "combien ca bouge par defaut".
|
||||||
|
IdleProfile = FHeadBobProfile(0.15f, 0.55f, 0.05f, 0.03f, 0.f);
|
||||||
|
WalkProfile = FHeadBobProfile(1.1f, 1.6f, 1.2f, 0.4f, 0.f);
|
||||||
|
SprintProfile = FHeadBobProfile(1.55f, 2.1f, 1.7f, 0.8f, 0.f);
|
||||||
|
CrouchProfile = FHeadBobProfile(0.7f, 1.f, 0.9f, 0.3f, 0.f);
|
||||||
|
AirProfile = FHeadBobProfile(0.f, 0.f, 0.f, 0.f, 0.f);
|
||||||
|
|
||||||
|
// On demarre deja sur le profil d'immobilite : partir de zeros ferait monter
|
||||||
|
// la respiration pendant la premiere seconde de jeu, ce qui se voit.
|
||||||
|
BlendedProfile = IdleProfile;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UHeadBobComponent::BeginPlay()
|
||||||
|
{
|
||||||
|
Super::BeginPlay();
|
||||||
|
|
||||||
|
OwnerCharacter = Cast<ACharacter>(GetOwner());
|
||||||
|
if (!OwnerCharacter)
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Warning, TEXT("UHeadBobComponent : le proprietaire n'est pas un ACharacter, le balancement de tete est desactive."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCharacterMovement() est un simple accesseur sur un membre du Character,
|
||||||
|
// mais le stocker evite un appel indirect par frame pour rien.
|
||||||
|
OwnerMovement = OwnerCharacter->GetCharacterMovement();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UHeadBobComponent::UpdateBob(float DeltaTime)
|
||||||
|
{
|
||||||
|
// IsLocallyControlled : chez l'hote, le pawn de chaque ami porte AUSSI ce
|
||||||
|
// composant. Sans cette garde, le serveur calculerait trois balancements pour
|
||||||
|
// des cameras que personne ne regarde.
|
||||||
|
if (!bEnableHeadBob || !OwnerCharacter || !OwnerMovement || !OwnerCharacter->IsLocallyControlled())
|
||||||
|
{
|
||||||
|
// Remise a zero et pas simple sortie : couper l'effet en pleine partie
|
||||||
|
// laisserait sinon la camera figee sur le dernier decalage calcule.
|
||||||
|
LocationOffset = FVector::ZeroVector;
|
||||||
|
RotationOffset = FRotator::ZeroRotator;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Meme garde-fou que les ressorts de camera : un hoquet de framerate ferait
|
||||||
|
// sauter la phase de plusieurs pas d'un coup.
|
||||||
|
const float Step = FMath::Min(DeltaTime, 1.f / 30.f);
|
||||||
|
|
||||||
|
BlendTowards(SelectTargetProfile(), Step);
|
||||||
|
|
||||||
|
// La phase avance a la frequence FONDUE, pas a celle du profil vise : c'est
|
||||||
|
// ce qui fait que le rythme des pas accelere progressivement au demarrage du
|
||||||
|
// sprint au lieu de doubler d'une frame a l'autre.
|
||||||
|
StridePhase = FMath::Fmod(StridePhase + BlendedProfile.StrideFrequency * 2.f * PI * Step, 2.f * PI);
|
||||||
|
|
||||||
|
const float Intensity = GlobalIntensity * IntensityScale;
|
||||||
|
|
||||||
|
// Une foulee = deux pas. Le balancement lateral suit la foulee entiere (on
|
||||||
|
// bascule sur un pied puis sur l'autre), la montee-descente bat deux fois plus
|
||||||
|
// vite, une bosse par pied qui touche le sol. C'est ce rapport 1:2 qui dessine
|
||||||
|
// le fameux 8 couche et qui fait qu'on lit une marche plutot qu'un flottement.
|
||||||
|
const float LateralWave = FMath::Sin(StridePhase);
|
||||||
|
const float VerticalWave = FMath::Sin(StridePhase * 2.f);
|
||||||
|
|
||||||
|
// X reste nul : une composante avant-arriere donnerait un effet de zoom qui,
|
||||||
|
// en vue premiere personne, se lit comme du mal de mer plus que comme un pas.
|
||||||
|
LocationOffset = FVector(
|
||||||
|
0.f,
|
||||||
|
LateralWave * BlendedProfile.LateralAmount * Intensity,
|
||||||
|
VerticalWave * BlendedProfile.VerticalAmount * Intensity);
|
||||||
|
|
||||||
|
// Le roulis suit le balancement lateral : la tete penche du cote ou le corps
|
||||||
|
// se porte. Le tangage suit la montee-descente, et vaut 0 par defaut.
|
||||||
|
RotationOffset = FRotator(
|
||||||
|
VerticalWave * BlendedProfile.PitchAmount * Intensity,
|
||||||
|
0.f,
|
||||||
|
LateralWave * BlendedProfile.RollAmount * Intensity);
|
||||||
|
}
|
||||||
|
|
||||||
|
const FHeadBobProfile& UHeadBobComponent::SelectTargetProfile() const
|
||||||
|
{
|
||||||
|
if (OwnerMovement->IsFalling())
|
||||||
|
{
|
||||||
|
return AirProfile;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vitesse a plat : la composante verticale d'une pente ou d'un ascenseur n'a
|
||||||
|
// rien a voir avec la cadence des pas.
|
||||||
|
const float GroundSpeed = OwnerMovement->Velocity.Size2D();
|
||||||
|
|
||||||
|
// L'immobilite passe AVANT l'accroupissement : accroupi sans bouger, on
|
||||||
|
// respire, on ne marche pas sur place.
|
||||||
|
if (GroundSpeed < IdleSpeedThreshold)
|
||||||
|
{
|
||||||
|
return IdleProfile;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (OwnerCharacter->bIsCrouched)
|
||||||
|
{
|
||||||
|
return CrouchProfile;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (GroundSpeed > SprintSpeedThreshold) ? SprintProfile : WalkProfile;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UHeadBobComponent::BlendTowards(const FHeadBobProfile& Target, float Step)
|
||||||
|
{
|
||||||
|
BlendedProfile.StrideFrequency = FMath::FInterpTo(BlendedProfile.StrideFrequency, Target.StrideFrequency, Step, ProfileBlendSpeed);
|
||||||
|
BlendedProfile.VerticalAmount = FMath::FInterpTo(BlendedProfile.VerticalAmount, Target.VerticalAmount, Step, ProfileBlendSpeed);
|
||||||
|
BlendedProfile.LateralAmount = FMath::FInterpTo(BlendedProfile.LateralAmount, Target.LateralAmount, Step, ProfileBlendSpeed);
|
||||||
|
BlendedProfile.RollAmount = FMath::FInterpTo(BlendedProfile.RollAmount, Target.RollAmount, Step, ProfileBlendSpeed);
|
||||||
|
BlendedProfile.PitchAmount = FMath::FInterpTo(BlendedProfile.PitchAmount, Target.PitchAmount, Step, ProfileBlendSpeed);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UHeadBobComponent::ResetBob()
|
||||||
|
{
|
||||||
|
StridePhase = 0.f;
|
||||||
|
BlendedProfile = IdleProfile;
|
||||||
|
LocationOffset = FVector::ZeroVector;
|
||||||
|
RotationOffset = FRotator::ZeroRotator;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UHeadBobComponent::SetIntensityScale(float NewScale)
|
||||||
|
{
|
||||||
|
IntensityScale = FMath::Max(NewScale, 0.f);
|
||||||
|
}
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
// Fill out your copyright notice in the Description page of Project Settings.
|
||||||
|
|
||||||
|
|
||||||
|
#include "KeyboardLayout.h"
|
||||||
|
|
||||||
|
#if PLATFORM_WINDOWS
|
||||||
|
// WindowsHWrapper et pas un <Windows.h> nu : il pose lui-meme les gardes
|
||||||
|
// Pre/PostWindowsApi qui neutralisent les macros et les typedefs de l'API Win32
|
||||||
|
// entrant en collision avec ceux du moteur (INT, FLOAT, TEXT...).
|
||||||
|
#include "Windows/WindowsHWrapper.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace KeyboardLayout
|
||||||
|
{
|
||||||
|
#if PLATFORM_WINDOWS
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Position physique de chaque touche de reference, en scancode PS/2 set 1.
|
||||||
|
*
|
||||||
|
* Ces codes decrivent l'EMPLACEMENT sur le clavier, pas le caractere grave
|
||||||
|
* dessus : c'est ce qui rend la table independante de la disposition. Ils sont
|
||||||
|
* figes depuis le clavier IBM de 1984 et ne bougeront plus.
|
||||||
|
*
|
||||||
|
* Table ecrite a la main plutot que deduite d'un HKL americain charge a la
|
||||||
|
* volee : LoadKeyboardLayout ajoute la disposition a celles du systeme, et un
|
||||||
|
* jeu n'a pas a toucher a la configuration clavier de la machine.
|
||||||
|
*
|
||||||
|
* Construite au PREMIER APPEL et pas en variable globale : les constantes de
|
||||||
|
* EKeys sont initialisees par le moteur au demarrage, et une table globale
|
||||||
|
* pourrait naitre avant elles.
|
||||||
|
*/
|
||||||
|
static const TMap<FName, uint32>& GetReferenceScanCodes()
|
||||||
|
{
|
||||||
|
static const TMap<FName, uint32> ScanCodes = []
|
||||||
|
{
|
||||||
|
TMap<FName, uint32> Map;
|
||||||
|
Map.Reserve(48);
|
||||||
|
|
||||||
|
// Rangee des chiffres
|
||||||
|
Map.Add(EKeys::Tilde.GetFName(), 0x29);
|
||||||
|
Map.Add(EKeys::One.GetFName(), 0x02);
|
||||||
|
Map.Add(EKeys::Two.GetFName(), 0x03);
|
||||||
|
Map.Add(EKeys::Three.GetFName(), 0x04);
|
||||||
|
Map.Add(EKeys::Four.GetFName(), 0x05);
|
||||||
|
Map.Add(EKeys::Five.GetFName(), 0x06);
|
||||||
|
Map.Add(EKeys::Six.GetFName(), 0x07);
|
||||||
|
Map.Add(EKeys::Seven.GetFName(), 0x08);
|
||||||
|
Map.Add(EKeys::Eight.GetFName(), 0x09);
|
||||||
|
Map.Add(EKeys::Nine.GetFName(), 0x0A);
|
||||||
|
Map.Add(EKeys::Zero.GetFName(), 0x0B);
|
||||||
|
Map.Add(EKeys::Hyphen.GetFName(), 0x0C);
|
||||||
|
Map.Add(EKeys::Equals.GetFName(), 0x0D);
|
||||||
|
|
||||||
|
// Rangee du haut
|
||||||
|
Map.Add(EKeys::Q.GetFName(), 0x10);
|
||||||
|
Map.Add(EKeys::W.GetFName(), 0x11);
|
||||||
|
Map.Add(EKeys::E.GetFName(), 0x12);
|
||||||
|
Map.Add(EKeys::R.GetFName(), 0x13);
|
||||||
|
Map.Add(EKeys::T.GetFName(), 0x14);
|
||||||
|
Map.Add(EKeys::Y.GetFName(), 0x15);
|
||||||
|
Map.Add(EKeys::U.GetFName(), 0x16);
|
||||||
|
Map.Add(EKeys::I.GetFName(), 0x17);
|
||||||
|
Map.Add(EKeys::O.GetFName(), 0x18);
|
||||||
|
Map.Add(EKeys::P.GetFName(), 0x19);
|
||||||
|
Map.Add(EKeys::LeftBracket.GetFName(), 0x1A);
|
||||||
|
Map.Add(EKeys::RightBracket.GetFName(), 0x1B);
|
||||||
|
Map.Add(EKeys::Backslash.GetFName(), 0x2B);
|
||||||
|
|
||||||
|
// Rangee de repos
|
||||||
|
Map.Add(EKeys::A.GetFName(), 0x1E);
|
||||||
|
Map.Add(EKeys::S.GetFName(), 0x1F);
|
||||||
|
Map.Add(EKeys::D.GetFName(), 0x20);
|
||||||
|
Map.Add(EKeys::F.GetFName(), 0x21);
|
||||||
|
Map.Add(EKeys::G.GetFName(), 0x22);
|
||||||
|
Map.Add(EKeys::H.GetFName(), 0x23);
|
||||||
|
Map.Add(EKeys::J.GetFName(), 0x24);
|
||||||
|
Map.Add(EKeys::K.GetFName(), 0x25);
|
||||||
|
Map.Add(EKeys::L.GetFName(), 0x26);
|
||||||
|
Map.Add(EKeys::Semicolon.GetFName(), 0x27);
|
||||||
|
Map.Add(EKeys::Apostrophe.GetFName(), 0x28);
|
||||||
|
|
||||||
|
// Rangee du bas
|
||||||
|
Map.Add(EKeys::Z.GetFName(), 0x2C);
|
||||||
|
Map.Add(EKeys::X.GetFName(), 0x2D);
|
||||||
|
Map.Add(EKeys::C.GetFName(), 0x2E);
|
||||||
|
Map.Add(EKeys::V.GetFName(), 0x2F);
|
||||||
|
Map.Add(EKeys::B.GetFName(), 0x30);
|
||||||
|
Map.Add(EKeys::N.GetFName(), 0x31);
|
||||||
|
Map.Add(EKeys::M.GetFName(), 0x32);
|
||||||
|
Map.Add(EKeys::Comma.GetFName(), 0x33);
|
||||||
|
Map.Add(EKeys::Period.GetFName(), 0x34);
|
||||||
|
Map.Add(EKeys::Slash.GetFName(), 0x35);
|
||||||
|
|
||||||
|
return Map;
|
||||||
|
}();
|
||||||
|
|
||||||
|
return ScanCodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // PLATFORM_WINDOWS
|
||||||
|
|
||||||
|
FKey RemapKeyToActiveLayout(const FKey& ReferenceKey)
|
||||||
|
{
|
||||||
|
#if PLATFORM_WINDOWS
|
||||||
|
const uint32* ScanCode = GetReferenceScanCodes().Find(ReferenceKey.GetFName());
|
||||||
|
if (!ScanCode)
|
||||||
|
{
|
||||||
|
// Touche insensible a la disposition : espace, modificateur, manette.
|
||||||
|
return ReferenceKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetKeyboardLayout(0) = la disposition du thread appelant, donc celle que
|
||||||
|
// le joueur a reellement active -- et pas celle par defaut du systeme, qui
|
||||||
|
// differe des qu'il en a plusieurs et bascule au clavier.
|
||||||
|
const HKL ActiveLayout = ::GetKeyboardLayout(0);
|
||||||
|
|
||||||
|
const UINT VirtualKey = ::MapVirtualKeyExW(*ScanCode, MAPVK_VSC_TO_VK_EX, ActiveLayout);
|
||||||
|
if (VirtualKey == 0)
|
||||||
|
{
|
||||||
|
// La disposition n'a rien a cette position. Rare, mais possible sur les
|
||||||
|
// dispositions non latines : on garde la touche d'origine.
|
||||||
|
return ReferenceKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
// On REPRODUIT ici, a l'identique, le couple que le moteur recevra au
|
||||||
|
// runtime : FWindowsApplication passe le virtual key ET le caractere non
|
||||||
|
// modifie rendu par MAPVK_VK_TO_CHAR. Deduire le CharCode du virtual key
|
||||||
|
// serait faux des que la touche ne porte pas le caractere de son code --
|
||||||
|
// et c'est precisement le cas de la rangee des chiffres en AZERTY, ou
|
||||||
|
// Windows garde VK_1 pour une touche qui produit "&". Le moteur y repond
|
||||||
|
// EKeys::Ampersand ; simuler EKeys::One nous ferait conclure qu'il n'y a
|
||||||
|
// rien a convertir, et les dix touches de la barre resteraient muettes.
|
||||||
|
//
|
||||||
|
// Le bit de poids fort signale une touche morte (l'accent circonflexe d'un
|
||||||
|
// AZERTY, par exemple) : on le masque, seul le caractere nous interesse.
|
||||||
|
const UINT CharCode = ::MapVirtualKeyExW(VirtualKey, MAPVK_VK_TO_CHAR, ActiveLayout) & 0x7FFFFFFF;
|
||||||
|
|
||||||
|
const FKey MappedKey = FInputKeyManager::Get().GetKeyFromCodes(VirtualKey, CharCode);
|
||||||
|
|
||||||
|
// Une touche introuvable dans les deux tables est SYNTHETISEE par le moteur,
|
||||||
|
// et elle sort marquee non bindable -- l'assigner donnerait une action
|
||||||
|
// definitivement muette. C'est le seul cas ou l'on prefere une touche fausse
|
||||||
|
// a une touche morte.
|
||||||
|
if (!MappedKey.IsValid() || !MappedKey.IsBindableToActions())
|
||||||
|
{
|
||||||
|
return ReferenceKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
return MappedKey;
|
||||||
|
#else
|
||||||
|
return ReferenceKey;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
FString GetActiveLayoutId()
|
||||||
|
{
|
||||||
|
#if PLATFORM_WINDOWS
|
||||||
|
// 16 et pas KL_NAMELENGTH : l'API en ecrit 9 au plus, et un buffer en dur
|
||||||
|
// evite de dependre d'une macro que WIN32_LEAN_AND_MEAN pourrait retirer.
|
||||||
|
WCHAR LayoutName[16] = {};
|
||||||
|
if (::GetKeyboardLayoutNameW(LayoutName))
|
||||||
|
{
|
||||||
|
return FString(LayoutName);
|
||||||
|
}
|
||||||
|
|
||||||
|
return TEXT("inconnue");
|
||||||
|
#else
|
||||||
|
return TEXT("non gere sur cette plateforme");
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,8 @@
|
|||||||
#include "Kismet/GameplayStatics.h"
|
#include "Kismet/GameplayStatics.h"
|
||||||
#include "MusicPlaylistDataAsset.h"
|
#include "MusicPlaylistDataAsset.h"
|
||||||
#include "Sound/SoundBase.h"
|
#include "Sound/SoundBase.h"
|
||||||
|
#include "Sound/SoundClass.h"
|
||||||
|
#include "SurvivalAudioSettings.h"
|
||||||
#include "UObject/UObjectGlobals.h"
|
#include "UObject/UObjectGlobals.h"
|
||||||
|
|
||||||
UMusicSubsystem* UMusicSubsystem::Get(const UObject* WorldContextObject)
|
UMusicSubsystem* UMusicSubsystem::Get(const UObject* WorldContextObject)
|
||||||
@@ -298,6 +300,20 @@ void UMusicSubsystem::PlayTrackAtCursor(float FadeInDuration)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Un curseur de volume ne doit RIEN faire d'autre que changer le volume.
|
||||||
|
//
|
||||||
|
// Sans PlayWhenSilent, une source devenue inaudible -- curseur Musique a zero --
|
||||||
|
// est declaree TERMINEE par le moteur : OnAudioFinished part, HandleTrackFinished
|
||||||
|
// enchaîne, et le silence entre deux morceaux (MinGapBetweenTracks) demarre. Le
|
||||||
|
// joueur qui remonte son volume n'entend alors plus rien jusqu'a la piste
|
||||||
|
// suivante, alors qu'il n'a fait que bouger un reglage. Avec ce mode le morceau
|
||||||
|
// continue d'avancer sans etre rendu, et redevient simplement audible.
|
||||||
|
//
|
||||||
|
// Pose ICI et pas sur les assets, pour la meme raison que la SoundClass dans
|
||||||
|
// CreateMusicComponent : ajouter une piste a une playlist ne doit rien demander
|
||||||
|
// de plus. Ecrire ce champ ne salit pas l'asset -- rien n'appelle MarkPackageDirty.
|
||||||
|
Track->VirtualizationMode = EVirtualizationMode::PlayWhenSilent;
|
||||||
|
|
||||||
if (!MusicComponent)
|
if (!MusicComponent)
|
||||||
{
|
{
|
||||||
MusicComponent = CreateMusicComponent(Track);
|
MusicComponent = CreateMusicComponent(Track);
|
||||||
@@ -353,6 +369,14 @@ UAudioComponent* UMusicSubsystem::CreateMusicComponent(USoundBase* Sound)
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// La classe est imposee ICI plutot que sur chaque asset de morceau : ajouter une
|
||||||
|
// piste a une playlist ne doit pas demander de penser a la taguer, sinon le
|
||||||
|
// curseur Musique cesse d'agir sur elle sans le moindre signe.
|
||||||
|
if (USoundClass* MusicClass = USurvivalAudioSettings::Get()->GetMusicSoundClass())
|
||||||
|
{
|
||||||
|
Component->SoundClassOverride = MusicClass;
|
||||||
|
}
|
||||||
|
|
||||||
Component->bIsMusic = true;
|
Component->bIsMusic = true;
|
||||||
Component->OnAudioFinished.AddDynamic(this, &UMusicSubsystem::HandleTrackFinished);
|
Component->OnAudioFinished.AddDynamic(this, &UMusicSubsystem::HandleTrackFinished);
|
||||||
return Component;
|
return Component;
|
||||||
|
|||||||
@@ -0,0 +1,181 @@
|
|||||||
|
// Fill out your copyright notice in the Description page of Project Settings.
|
||||||
|
|
||||||
|
|
||||||
|
#include "SettingsAudioWidget.h"
|
||||||
|
|
||||||
|
#include "SettingRowWidget.h"
|
||||||
|
#include "SurvivalUserSettings.h"
|
||||||
|
|
||||||
|
#define LOCTEXT_NAMESPACE "Reglages"
|
||||||
|
|
||||||
|
namespace SettingsAudioTab
|
||||||
|
{
|
||||||
|
/** La seule liste de choix de cet onglet. */
|
||||||
|
TArray<FText> MakeMuteAudibleLabels()
|
||||||
|
{
|
||||||
|
return { LOCTEXT("Muet", "Muet"), LOCTEXT("Audible", "Audible") };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void USettingsAudioWidget::NativeConstruct()
|
||||||
|
{
|
||||||
|
Super::NativeConstruct();
|
||||||
|
|
||||||
|
using namespace SurvivalSettingsRange;
|
||||||
|
using namespace SettingsAudioTab;
|
||||||
|
|
||||||
|
const FText PercentSuffix = LOCTEXT("Pourcent", " %");
|
||||||
|
|
||||||
|
if (MasterVolumeRow)
|
||||||
|
{
|
||||||
|
MasterVolumeRow->SetupAsNumber(LOCTEXT("VolumeGeneral", "Volume général"),
|
||||||
|
VolumeMin, VolumeMax, VolumeStep, VolumeArrowStep, VolumeMin, PercentSuffix);
|
||||||
|
MasterVolumeRow->OnIndexChanged.AddDynamic(this, &USettingsAudioWidget::HandleMasterVolumeChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (MusicVolumeRow)
|
||||||
|
{
|
||||||
|
MusicVolumeRow->SetupAsNumber(LOCTEXT("Musique", "Musique"),
|
||||||
|
VolumeMin, VolumeMax, VolumeStep, VolumeArrowStep, VolumeMin, PercentSuffix);
|
||||||
|
MusicVolumeRow->OnIndexChanged.AddDynamic(this, &USettingsAudioWidget::HandleMusicVolumeChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (EffectsVolumeRow)
|
||||||
|
{
|
||||||
|
// Cle "EffetsSonores" et pas "Effets" : l'onglet Graphismes utilise deja
|
||||||
|
// cette derniere pour la qualite des effets visuels, dans le meme namespace.
|
||||||
|
EffectsVolumeRow->SetupAsNumber(LOCTEXT("EffetsSonores", "Effets sonores"),
|
||||||
|
VolumeMin, VolumeMax, VolumeStep, VolumeArrowStep, VolumeMin, PercentSuffix);
|
||||||
|
EffectsVolumeRow->OnIndexChanged.AddDynamic(this, &USettingsAudioWidget::HandleEffectsVolumeChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (UiVolumeRow)
|
||||||
|
{
|
||||||
|
UiVolumeRow->SetupAsNumber(LOCTEXT("Interface", "Interface"),
|
||||||
|
VolumeMin, VolumeMax, VolumeStep, VolumeArrowStep, VolumeMin, PercentSuffix);
|
||||||
|
UiVolumeRow->OnIndexChanged.AddDynamic(this, &USettingsAudioWidget::HandleUiVolumeChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (BackgroundSoundRow)
|
||||||
|
{
|
||||||
|
BackgroundSoundRow->SetupAsChoice(LOCTEXT("SonEnArrierePlan", "Jeu en arrière-plan"),
|
||||||
|
MakeMuteAudibleLabels(), 0);
|
||||||
|
BackgroundSoundRow->OnIndexChanged.AddDynamic(this, &USettingsAudioWidget::HandleBackgroundSoundChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
RefreshFromSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
void USettingsAudioWidget::RefreshFromSettings()
|
||||||
|
{
|
||||||
|
USurvivalUserSettings* Settings = USurvivalUserSettings::Get();
|
||||||
|
if (!Settings)
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Error, TEXT("USettingsAudioWidget : GameUserSettingsClassName n'est pas configure dans DefaultEngine.ini, l'onglet restera inerte."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetNumericValueSilently : diffuser ici ferait boucler l'onglet sur lui-meme.
|
||||||
|
if (MasterVolumeRow)
|
||||||
|
{
|
||||||
|
MasterVolumeRow->SetNumericValueSilently(Settings->GetMasterVolume());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (MusicVolumeRow)
|
||||||
|
{
|
||||||
|
MusicVolumeRow->SetNumericValueSilently(Settings->GetMusicVolume());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (EffectsVolumeRow)
|
||||||
|
{
|
||||||
|
EffectsVolumeRow->SetNumericValueSilently(Settings->GetEffectsVolume());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (UiVolumeRow)
|
||||||
|
{
|
||||||
|
UiVolumeRow->SetNumericValueSilently(Settings->GetUiVolume());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (BackgroundSoundRow)
|
||||||
|
{
|
||||||
|
BackgroundSoundRow->SetIndexSilently(Settings->GetPlayInBackground() ? 1 : 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void USettingsAudioWidget::ResetToDefaults()
|
||||||
|
{
|
||||||
|
USurvivalUserSettings* Settings = USurvivalUserSettings::Get();
|
||||||
|
if (!Settings)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Settings->SetSurvivalAudioToDefaults();
|
||||||
|
Settings->ApplyAudioSettings();
|
||||||
|
RefreshFromSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
void USettingsAudioWidget::HandleMasterVolumeChanged(int32 NewIndex)
|
||||||
|
{
|
||||||
|
USurvivalUserSettings* Settings = USurvivalUserSettings::Get();
|
||||||
|
if (!Settings || !MasterVolumeRow)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Settings->SetMasterVolume(MasterVolumeRow->GetNumericValue());
|
||||||
|
|
||||||
|
// Pas de SaveSettings ici : c'est l'ecran de reglages qui ecrit le .ini en se
|
||||||
|
// fermant. Meme regle que les autres onglets.
|
||||||
|
Settings->ApplyAudioSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
void USettingsAudioWidget::HandleMusicVolumeChanged(int32 NewIndex)
|
||||||
|
{
|
||||||
|
USurvivalUserSettings* Settings = USurvivalUserSettings::Get();
|
||||||
|
if (!Settings || !MusicVolumeRow)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Settings->SetMusicVolume(MusicVolumeRow->GetNumericValue());
|
||||||
|
Settings->ApplyAudioSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
void USettingsAudioWidget::HandleEffectsVolumeChanged(int32 NewIndex)
|
||||||
|
{
|
||||||
|
USurvivalUserSettings* Settings = USurvivalUserSettings::Get();
|
||||||
|
if (!Settings || !EffectsVolumeRow)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Settings->SetEffectsVolume(EffectsVolumeRow->GetNumericValue());
|
||||||
|
Settings->ApplyAudioSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
void USettingsAudioWidget::HandleUiVolumeChanged(int32 NewIndex)
|
||||||
|
{
|
||||||
|
USurvivalUserSettings* Settings = USurvivalUserSettings::Get();
|
||||||
|
if (!Settings || !UiVolumeRow)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aucun son de test a jouer : les fleches de la ligne portent deja le son de
|
||||||
|
// clic de l'interface, donc le joueur entend le reglage qu'il est en train de
|
||||||
|
// faire, sur le son exact qu'il regle.
|
||||||
|
Settings->SetUiVolume(UiVolumeRow->GetNumericValue());
|
||||||
|
Settings->ApplyAudioSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
void USettingsAudioWidget::HandleBackgroundSoundChanged(int32 NewIndex)
|
||||||
|
{
|
||||||
|
if (USurvivalUserSettings* Settings = USurvivalUserSettings::Get())
|
||||||
|
{
|
||||||
|
Settings->SetPlayInBackground(NewIndex == 1);
|
||||||
|
Settings->ApplyAudioSettings();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#undef LOCTEXT_NAMESPACE
|
||||||
@@ -186,6 +186,16 @@ void USettingsControlsWidget::ResetToDefaults()
|
|||||||
}
|
}
|
||||||
|
|
||||||
Settings->ApplySettings();
|
Settings->ApplySettings();
|
||||||
|
|
||||||
|
// Reinitialiser rend les touches de l'IMC, donc du QWERTY : sur un clavier
|
||||||
|
// AZERTY le joueur se retrouverait avec un WASD qu'il ne peut pas atteindre.
|
||||||
|
// On repose donc la disposition juste apres. "Par defaut" veut dire "par defaut
|
||||||
|
// POUR TON CLAVIER", pas "par defaut pour le clavier du developpeur".
|
||||||
|
if (USurvivalGameInstance* GameInstance = GetGameInstance<USurvivalGameInstance>())
|
||||||
|
{
|
||||||
|
GameInstance->ApplyKeyboardLayoutToDefaultKeys();
|
||||||
|
}
|
||||||
|
|
||||||
RefreshFromSettings();
|
RefreshFromSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,8 +18,8 @@
|
|||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
// Ordre des pages dans le WidgetSwitcher. Les trois dernieres n'existent pas
|
// Ordre des pages dans le WidgetSwitcher. Un bouton dont la page manque se
|
||||||
// encore : leur bouton se grise tout seul tant que la page est absente.
|
// grise tout seul, ce qui reste vrai si un onglet vient a etre retire.
|
||||||
constexpr int32 TabIndexGameplay = 0;
|
constexpr int32 TabIndexGameplay = 0;
|
||||||
constexpr int32 TabIndexDisplay = 1;
|
constexpr int32 TabIndexDisplay = 1;
|
||||||
constexpr int32 TabIndexGraphics = 2;
|
constexpr int32 TabIndexGraphics = 2;
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
// Fill out your copyright notice in the Description page of Project Settings.
|
||||||
|
|
||||||
|
|
||||||
|
#include "SurvivalAudioSettings.h"
|
||||||
|
|
||||||
|
#include "Sound/SoundClass.h"
|
||||||
|
|
||||||
|
FName USurvivalAudioSettings::GetCategoryName() const
|
||||||
|
{
|
||||||
|
return TEXT("Game");
|
||||||
|
}
|
||||||
|
|
||||||
|
USurvivalAudioSettings* USurvivalAudioSettings::Get()
|
||||||
|
{
|
||||||
|
// GetMutableDefault et non GetDefault : les champs Cached* sont remplis a la
|
||||||
|
// demande, donc les accesseurs ne peuvent pas etre const. Ce ne sont ni des
|
||||||
|
// UPROPERTY ni des donnees serialisees -- muter un CDO n'est un probleme que
|
||||||
|
// pour ce qui part sur le disque.
|
||||||
|
return GetMutableDefault<USurvivalAudioSettings>();
|
||||||
|
}
|
||||||
|
|
||||||
|
USoundClass* USurvivalAudioSettings::Resolve(const TSoftObjectPtr<USoundClass>& Asset, TStrongObjectPtr<USoundClass>& Cache)
|
||||||
|
{
|
||||||
|
if (Cache.IsValid())
|
||||||
|
{
|
||||||
|
return Cache.Get();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Asset.IsNull())
|
||||||
|
{
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Synchrone assume : une SoundClass est un objet minuscule, et le chargement
|
||||||
|
// n'a lieu qu'une fois par lancement.
|
||||||
|
//
|
||||||
|
// Reset et pas une affectation : c'est ce Reset qui pose l'ancre GC, et la
|
||||||
|
// raison pour laquelle ces caches ne sont pas des UPROPERTY (voir le header).
|
||||||
|
Cache.Reset(Asset.LoadSynchronous());
|
||||||
|
return Cache.Get();
|
||||||
|
}
|
||||||
|
|
||||||
|
USoundClass* USurvivalAudioSettings::GetMasterSoundClass()
|
||||||
|
{
|
||||||
|
return Resolve(MasterSoundClass, CachedMasterSoundClass);
|
||||||
|
}
|
||||||
|
|
||||||
|
USoundClass* USurvivalAudioSettings::GetMusicSoundClass()
|
||||||
|
{
|
||||||
|
return Resolve(MusicSoundClass, CachedMusicSoundClass);
|
||||||
|
}
|
||||||
|
|
||||||
|
USoundClass* USurvivalAudioSettings::GetEffectsSoundClass()
|
||||||
|
{
|
||||||
|
return Resolve(EffectsSoundClass, CachedEffectsSoundClass);
|
||||||
|
}
|
||||||
|
|
||||||
|
USoundClass* USurvivalAudioSettings::GetUiSoundClass()
|
||||||
|
{
|
||||||
|
return Resolve(UiSoundClass, CachedUiSoundClass);
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
#include "Engine/LocalPlayer.h"
|
#include "Engine/LocalPlayer.h"
|
||||||
#include "GameMapsSettings.h"
|
#include "GameMapsSettings.h"
|
||||||
#include "InputMappingContext.h"
|
#include "InputMappingContext.h"
|
||||||
|
#include "KeyboardLayout.h"
|
||||||
#include "Kismet/GameplayStatics.h"
|
#include "Kismet/GameplayStatics.h"
|
||||||
#include "SessionSubsystem.h"
|
#include "SessionSubsystem.h"
|
||||||
#include "SurvivalUserSettings.h"
|
#include "SurvivalUserSettings.h"
|
||||||
@@ -233,13 +234,131 @@ void USurvivalGameInstance::RegisterMappableContexts()
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool bRegisteredAnyContext = false;
|
||||||
|
|
||||||
for (const TObjectPtr<UInputMappingContext>& Context : MappableContexts)
|
for (const TObjectPtr<UInputMappingContext>& Context : MappableContexts)
|
||||||
{
|
{
|
||||||
if (Context)
|
if (Context)
|
||||||
{
|
{
|
||||||
|
bRegisteredAnyContext = true;
|
||||||
// Idempotent : le moteur ignore un contexte deja connu, on peut donc
|
// Idempotent : le moteur ignore un contexte deja connu, on peut donc
|
||||||
// rappeler cette fonction sans precaution.
|
// rappeler cette fonction sans precaution.
|
||||||
InputSettings->RegisterInputMappingContext(Context);
|
InputSettings->RegisterInputMappingContext(Context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// APRES l'enregistrement et jamais avant : le profil de touches ne connait un
|
||||||
|
// mapping -- donc sa touche par defaut -- qu'une fois son IMC declare. Appeler
|
||||||
|
// la conversion plus tot la ferait tourner sur un profil vide, sans erreur ni
|
||||||
|
// effet, ce qui est le pire des cas a diagnostiquer.
|
||||||
|
//
|
||||||
|
// UNE SEULE FOIS par session, en revanche. Cette fonction est aussi le point
|
||||||
|
// d'entree du rafraichissement de l'onglet Touches, appele apres CHAQUE
|
||||||
|
// remappage : y laisser la conversion reprendrait sa touche au joueur qui a
|
||||||
|
// choisi celle de l'IMC -- la lettre W sur un AZERTY est identique au defaut,
|
||||||
|
// donc indiscernable d'un mapping auquel personne n'a touche.
|
||||||
|
if (bRegisteredAnyContext && !bKeyboardLayoutApplied)
|
||||||
|
{
|
||||||
|
bKeyboardLayoutApplied = true;
|
||||||
|
ApplyKeyboardLayoutToDefaultKeys();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void USurvivalGameInstance::ApplyKeyboardLayoutToDefaultKeys()
|
||||||
|
{
|
||||||
|
const ULocalPlayer* LocalPlayer = GetFirstGamePlayer();
|
||||||
|
if (!LocalPlayer)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
UEnhancedInputLocalPlayerSubsystem* Subsystem = LocalPlayer->GetSubsystem<UEnhancedInputLocalPlayerSubsystem>();
|
||||||
|
UEnhancedInputUserSettings* InputSettings = Subsystem ? Subsystem->GetUserSettings() : nullptr;
|
||||||
|
const UEnhancedPlayerMappableKeyProfile* Profile = InputSettings ? InputSettings->GetActiveKeyProfile() : nullptr;
|
||||||
|
|
||||||
|
if (!Profile)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// On COLLECTE avant d'appliquer, au lieu de mapper au fil de la lecture.
|
||||||
|
// MapPlayerKey ecrit dans les memes lignes que celles qu'on parcourt : muter un
|
||||||
|
// conteneur pendant son iteration invalide les iterateurs des que le stockage
|
||||||
|
// est reorganise. Le bug qui en sort est intermittent et ne ressemble a rien.
|
||||||
|
struct FPendingRemap
|
||||||
|
{
|
||||||
|
FName MappingName;
|
||||||
|
EPlayerMappableKeySlot Slot;
|
||||||
|
FKey LayoutKey;
|
||||||
|
};
|
||||||
|
|
||||||
|
TArray<FPendingRemap> PendingRemaps;
|
||||||
|
|
||||||
|
for (const TPair<FName, FKeyMappingRow>& Row : Profile->GetPlayerMappingRows())
|
||||||
|
{
|
||||||
|
for (const FPlayerKeyMapping& Mapping : Row.Value.Mappings)
|
||||||
|
{
|
||||||
|
// Le joueur a deja choisi cette touche : son choix fait autorite, meme
|
||||||
|
// s'il est incoherent avec sa disposition. C'est aussi ce qui rend la
|
||||||
|
// fonction idempotente -- notre propre conversion compte comme un choix
|
||||||
|
// des qu'elle est posee, donc on ne repasse jamais dessus.
|
||||||
|
if (Mapping.IsCustomized())
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FKey DefaultKey = Mapping.GetDefaultKey();
|
||||||
|
const FKey LayoutKey = KeyboardLayout::RemapKeyToActiveLayout(DefaultKey);
|
||||||
|
|
||||||
|
// Clavier QWERTY, touche insensible a la disposition, ou manette : la
|
||||||
|
// conversion rend la touche d'origine et il n'y a rien a ecrire. Sans ce
|
||||||
|
// test on poserait une surcharge identique au defaut sur chaque mapping,
|
||||||
|
// et "Reinitialiser" n'aurait plus rien a reinitialiser.
|
||||||
|
if (LayoutKey == DefaultKey)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
PendingRemaps.Add({ Row.Key, Mapping.GetSlot(), LayoutKey });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Journalise AVANT de sortir, et donc meme quand il n'y a rien a faire. C'est
|
||||||
|
// la seule trace lisible quand un testeur signale des touches inattendues, et
|
||||||
|
// le cas "0 touche" est justement celui qu'il faut pouvoir distinguer du
|
||||||
|
// silence : sans cette ligne, un clavier QWERTY et un systeme qui n'a pas
|
||||||
|
// tourne du tout produisent exactement le meme log, c'est-a-dire aucun.
|
||||||
|
//
|
||||||
|
// Un KLID se lit en deux temps : "0000040C" est un AZERTY francais, "00000409"
|
||||||
|
// un QWERTY US. Un utilisateur francais sur clavier QWERTY rend bien 00000409,
|
||||||
|
// parce que c'est la DISPOSITION qui est journalisee, pas la langue -- et c'est
|
||||||
|
// exactement la distinction sur laquelle une detection par pays se tromperait.
|
||||||
|
UE_LOG(LogTemp, Log, TEXT("USurvivalGameInstance : disposition clavier [%s], %d touche(s) a traduire depuis le QWERTY de reference."),
|
||||||
|
*KeyboardLayout::GetActiveLayoutId(), PendingRemaps.Num());
|
||||||
|
|
||||||
|
if (PendingRemaps.IsEmpty())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const FPendingRemap& Remap : PendingRemaps)
|
||||||
|
{
|
||||||
|
FMapPlayerKeyArgs Args;
|
||||||
|
Args.MappingName = Remap.MappingName;
|
||||||
|
Args.Slot = Remap.Slot;
|
||||||
|
Args.NewKey = Remap.LayoutKey;
|
||||||
|
|
||||||
|
FGameplayTagContainer FailureReason;
|
||||||
|
InputSettings->MapPlayerKey(Args, FailureReason);
|
||||||
|
|
||||||
|
if (!FailureReason.IsEmpty())
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Warning, TEXT("USurvivalGameInstance : conversion de '%s' vers '%s' refusee (%s)."),
|
||||||
|
*Remap.MappingName.ToString(), *Remap.LayoutKey.ToString(), *FailureReason.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pousse les surcharges dans les contextes deja actifs : le pawn n'a rien a
|
||||||
|
// rebrancher, les touches sont bonnes des la premiere image.
|
||||||
|
InputSettings->ApplySettings();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,14 @@
|
|||||||
|
|
||||||
#include "SurvivalUserSettings.h"
|
#include "SurvivalUserSettings.h"
|
||||||
|
|
||||||
|
#include "AudioDevice.h"
|
||||||
#include "Engine/Engine.h"
|
#include "Engine/Engine.h"
|
||||||
|
#include "Engine/World.h"
|
||||||
|
#include "Misc/App.h"
|
||||||
|
#include "Sound/AudioSettings.h"
|
||||||
|
#include "Sound/SoundClass.h"
|
||||||
|
#include "Sound/SoundMix.h"
|
||||||
|
#include "SurvivalAudioSettings.h"
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
@@ -13,6 +20,88 @@ namespace
|
|||||||
const float Snapped = Min + FMath::RoundToFloat((Value - Min) / Step) * Step;
|
const float Snapped = Min + FMath::RoundToFloat((Value - Min) / Step) * Step;
|
||||||
return FMath::Clamp(Snapped, Min, Max);
|
return FMath::Clamp(Snapped, Min, Max);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Pourcentage affiche -> amplitude reelle, courbe perceptive comprise. */
|
||||||
|
float VolumePercentToAmplitude(float Percent)
|
||||||
|
{
|
||||||
|
using namespace SurvivalSettingsRange;
|
||||||
|
|
||||||
|
const float Normalized = FMath::Clamp(Percent, VolumeMin, VolumeMax) / 100.f;
|
||||||
|
return FMath::Pow(Normalized, VolumeCurveExponent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Le peripherique audio a qui parler.
|
||||||
|
*
|
||||||
|
* En PIE le monde de jeu peut avoir le sien, distinct de celui de l'editeur :
|
||||||
|
* viser systematiquement le peripherique principal ferait regler le volume de
|
||||||
|
* l'editeur pendant que le jeu, lui, ne bouge pas.
|
||||||
|
*/
|
||||||
|
FAudioDevice* GetSettingsAudioDevice()
|
||||||
|
{
|
||||||
|
if (!GEngine)
|
||||||
|
{
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (const UWorld* PlayWorld = GEngine->GetCurrentPlayWorld())
|
||||||
|
{
|
||||||
|
if (FAudioDevice* WorldDevice = PlayWorld->GetAudioDeviceRaw())
|
||||||
|
{
|
||||||
|
return WorldDevice;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return GEngine->GetMainAudioDeviceRaw();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Le mix par lequel passent les volumes du joueur.
|
||||||
|
*
|
||||||
|
* C'est celui de Project Settings > Audio > Default Base Sound Mix, et pas une
|
||||||
|
* reference a nous. Deux raisons : le moteur le REPOSE a chaque chargement de
|
||||||
|
* map (UWorld::UpdateWorldComponents -> SetDefaultBaseSoundMix), donc il est
|
||||||
|
* toujours actif sans qu'on ait a le pousser nous-memes ; et un mix de base est
|
||||||
|
* evalue a plein regime en permanence, alors qu'un mix pousse a la main
|
||||||
|
* s'eteindrait au bout de sa duree.
|
||||||
|
*/
|
||||||
|
USoundMix* GetSettingsSoundMix()
|
||||||
|
{
|
||||||
|
const FSoftObjectPath& MixPath = GetDefault<UAudioSettings>()->DefaultBaseSoundMix;
|
||||||
|
return MixPath.IsValid() ? Cast<USoundMix>(MixPath.TryLoad()) : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pose le volume d'une SoundClass, en pourcentage.
|
||||||
|
*
|
||||||
|
* bApplyToChildren merite une explication, parce que se tromper la donne un
|
||||||
|
* volume maitre qui n'agit que sur les sons non tagues : le moteur propage les
|
||||||
|
* proprietes du parent vers les enfants (ParseSoundClasses) AVANT d'appliquer
|
||||||
|
* les ajusteurs du mix (ApplyClassAdjusters). Un ajusteur pose sur la classe
|
||||||
|
* maitre arrive donc trop tard pour redescendre tout seul -- il faut demander
|
||||||
|
* explicitement la recursion. Les feuilles, elles, gardent false : leur
|
||||||
|
* ajusteur n'a personne a qui redescendre.
|
||||||
|
*
|
||||||
|
* Aucun fondu non plus, et ce n'est pas un oubli : toute duree non nulle rend le
|
||||||
|
* curseur inutilisable. Quand on repose un override alors que le precedent n'a
|
||||||
|
* pas fini son fondu, UpdateClassAdjustorOverrideEntry relance l'interpolation
|
||||||
|
* (FDynamicParameter::Set remet CurrTimeSec a zero) mais n'appelle PAS
|
||||||
|
* Update(DeltaTime) cette frame-la. Un curseur qu'on glisse repose l'override a
|
||||||
|
* chaque image : la rampe repart indefiniment de zero et GetValue() rend toujours
|
||||||
|
* la valeur precedente. Le volume ne rattrape son retard qu'une fois le geste
|
||||||
|
* ARRETE. A zero, Set() ecrit directement la valeur -- et le mixeur lisse deja le
|
||||||
|
* gain de chaque source dans son buffer de rendu, donc pas de clic pour autant.
|
||||||
|
*/
|
||||||
|
void ApplyClassVolume(FAudioDevice& AudioDevice, USoundMix& Mix, USoundClass* SoundClass, float Percent, bool bApplyToChildren)
|
||||||
|
{
|
||||||
|
if (!SoundClass)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioDevice.SetSoundMixClassOverride(&Mix, SoundClass,
|
||||||
|
VolumePercentToAmplitude(Percent), /*Pitch*/ 1.f, /*FadeInTime*/ 0.f, bApplyToChildren);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
USurvivalUserSettings::USurvivalUserSettings()
|
USurvivalUserSettings::USurvivalUserSettings()
|
||||||
@@ -22,6 +111,7 @@ USurvivalUserSettings::USurvivalUserSettings()
|
|||||||
// fichier s'il existe.
|
// fichier s'il existe.
|
||||||
SetSurvivalGameplayToDefaults();
|
SetSurvivalGameplayToDefaults();
|
||||||
SetSurvivalDisplayToDefaults();
|
SetSurvivalDisplayToDefaults();
|
||||||
|
SetSurvivalAudioToDefaults();
|
||||||
}
|
}
|
||||||
|
|
||||||
USurvivalUserSettings* USurvivalUserSettings::Get()
|
USurvivalUserSettings* USurvivalUserSettings::Get()
|
||||||
@@ -46,6 +136,22 @@ void USurvivalUserSettings::SetSurvivalDisplayToDefaults()
|
|||||||
Brightness = 100.f;
|
Brightness = 100.f;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void USurvivalUserSettings::SetSurvivalAudioToDefaults()
|
||||||
|
{
|
||||||
|
// 80 et non 100 sur le maitre : il faut laisser au joueur de la marge vers le
|
||||||
|
// haut, sinon le seul reglage possible est de baisser.
|
||||||
|
MasterVolume = 80.f;
|
||||||
|
|
||||||
|
// La musique sous les effets, comme partout : elle est continue, eux sont des
|
||||||
|
// evenements, et un evenement doit passer par-dessus le fond.
|
||||||
|
MusicVolume = 60.f;
|
||||||
|
EffectsVolume = 100.f;
|
||||||
|
UiVolume = 80.f;
|
||||||
|
|
||||||
|
// Le defaut du moteur, qu'on se contente d'exposer.
|
||||||
|
bPlayInBackground = false;
|
||||||
|
}
|
||||||
|
|
||||||
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 :
|
||||||
@@ -54,6 +160,7 @@ void USurvivalUserSettings::SetToDefaults()
|
|||||||
|
|
||||||
SetSurvivalGameplayToDefaults();
|
SetSurvivalGameplayToDefaults();
|
||||||
SetSurvivalDisplayToDefaults();
|
SetSurvivalDisplayToDefaults();
|
||||||
|
SetSurvivalAudioToDefaults();
|
||||||
}
|
}
|
||||||
|
|
||||||
void USurvivalUserSettings::SetBrightness(float Value)
|
void USurvivalUserSettings::SetBrightness(float Value)
|
||||||
@@ -85,6 +192,40 @@ void USurvivalUserSettings::ApplySurvivalSettings()
|
|||||||
OnSurvivalSettingsApplied.Broadcast();
|
OnSurvivalSettingsApplied.Broadcast();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void USurvivalUserSettings::ApplyAudioSettings()
|
||||||
|
{
|
||||||
|
// Ce reglage-la ne passe pas par le mix : c'est l'application entiere qui est
|
||||||
|
// baissee quand la fenetre perd le focus, bien en amont des SoundClass. Le
|
||||||
|
// moteur le stocke dans son propre .ini, d'ou une valeur reposee a chaque
|
||||||
|
// application plutot que memorisee ailleurs.
|
||||||
|
FApp::SetUnfocusedVolumeMultiplier(bPlayInBackground ? 1.f : 0.f);
|
||||||
|
|
||||||
|
FAudioDevice* AudioDevice = GetSettingsAudioDevice();
|
||||||
|
if (!AudioDevice)
|
||||||
|
{
|
||||||
|
// Normal tres tot au demarrage, ou en serveur dedie : les valeurs sont
|
||||||
|
// deja enregistrees, le prochain passage les posera.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
USoundMix* Mix = GetSettingsSoundMix();
|
||||||
|
if (!Mix)
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Warning, TEXT("USurvivalUserSettings : aucun Default Base Sound Mix dans Project Settings > Audio, les curseurs de volume resteront sans effet."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
USurvivalAudioSettings* AudioAssets = USurvivalAudioSettings::Get();
|
||||||
|
|
||||||
|
// Le maitre EN PREMIER et avec recursion : il redescend sur les trois autres,
|
||||||
|
// qui reappliquent ensuite la leur par-dessus. Les deux se multiplient, ce qui
|
||||||
|
// est exactement ce qu'on attend d'un volume general.
|
||||||
|
ApplyClassVolume(*AudioDevice, *Mix, AudioAssets->GetMasterSoundClass(), MasterVolume, /*bApplyToChildren*/ true);
|
||||||
|
ApplyClassVolume(*AudioDevice, *Mix, AudioAssets->GetMusicSoundClass(), MusicVolume, /*bApplyToChildren*/ false);
|
||||||
|
ApplyClassVolume(*AudioDevice, *Mix, AudioAssets->GetEffectsSoundClass(), EffectsVolume, /*bApplyToChildren*/ false);
|
||||||
|
ApplyClassVolume(*AudioDevice, *Mix, AudioAssets->GetUiSoundClass(), UiVolume, /*bApplyToChildren*/ false);
|
||||||
|
}
|
||||||
|
|
||||||
void USurvivalUserSettings::ApplyNonResolutionSettings()
|
void USurvivalUserSettings::ApplyNonResolutionSettings()
|
||||||
{
|
{
|
||||||
Super::ApplyNonResolutionSettings();
|
Super::ApplyNonResolutionSettings();
|
||||||
@@ -92,6 +233,7 @@ void USurvivalUserSettings::ApplyNonResolutionSettings()
|
|||||||
// ApplySettings() passe forcement par ici : brancher la diffusion a ce
|
// ApplySettings() passe forcement par ici : brancher la diffusion a ce
|
||||||
// niveau garantit que nos reglages suivent, quel que soit le chemin.
|
// niveau garantit que nos reglages suivent, quel que soit le chemin.
|
||||||
ApplySurvivalSettings();
|
ApplySurvivalSettings();
|
||||||
|
ApplyAudioSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
float USurvivalUserSettings::GetLookMultiplierX() const
|
float USurvivalUserSettings::GetLookMultiplierX() const
|
||||||
@@ -173,3 +315,32 @@ void USurvivalUserSettings::SetCrouchToggle(bool bValue)
|
|||||||
{
|
{
|
||||||
bCrouchToggle = bValue;
|
bCrouchToggle = bValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void USurvivalUserSettings::SetMasterVolume(float Value)
|
||||||
|
{
|
||||||
|
using namespace SurvivalSettingsRange;
|
||||||
|
MasterVolume = SnapToStep(Value, VolumeMin, VolumeMax, VolumeStep);
|
||||||
|
}
|
||||||
|
|
||||||
|
void USurvivalUserSettings::SetMusicVolume(float Value)
|
||||||
|
{
|
||||||
|
using namespace SurvivalSettingsRange;
|
||||||
|
MusicVolume = SnapToStep(Value, VolumeMin, VolumeMax, VolumeStep);
|
||||||
|
}
|
||||||
|
|
||||||
|
void USurvivalUserSettings::SetEffectsVolume(float Value)
|
||||||
|
{
|
||||||
|
using namespace SurvivalSettingsRange;
|
||||||
|
EffectsVolume = SnapToStep(Value, VolumeMin, VolumeMax, VolumeStep);
|
||||||
|
}
|
||||||
|
|
||||||
|
void USurvivalUserSettings::SetUiVolume(float Value)
|
||||||
|
{
|
||||||
|
using namespace SurvivalSettingsRange;
|
||||||
|
UiVolume = SnapToStep(Value, VolumeMin, VolumeMax, VolumeStep);
|
||||||
|
}
|
||||||
|
|
||||||
|
void USurvivalUserSettings::SetPlayInBackground(bool bValue)
|
||||||
|
{
|
||||||
|
bPlayInBackground = bValue;
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
// Fill out your copyright notice in the Description page of Project Settings.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "Animation/AnimInstance.h"
|
||||||
|
#include "FpsArmsAnimInstance.generated.h"
|
||||||
|
|
||||||
|
class AFpsPlayer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AnimInstance des bras en vue premiere personne.
|
||||||
|
*
|
||||||
|
* Elle ne DECIDE de rien : elle lit l'etat du pawn une fois par frame et le
|
||||||
|
* republie sous forme de variables. C'est la machine a etats du Blueprint enfant
|
||||||
|
* (ABP_FpsArms) qui choisit l'animation. La separation est volontaire -- une
|
||||||
|
* transition d'animation se regle a l'oeil, avec des durees de fondu, et ca n'a
|
||||||
|
* rien a faire dans du C++ qu'il faut recompiler.
|
||||||
|
*
|
||||||
|
* Corollaire : ces variables ne sont JAMAIS repliquees. Les bras ne sont vus que
|
||||||
|
* par leur proprietaire, donc chaque machine calcule les siens a partir d'un etat
|
||||||
|
* de pawn que la replication a deja synchronise (vitesse, sprint, accroupi).
|
||||||
|
*/
|
||||||
|
UCLASS()
|
||||||
|
class SURVIVAL_PROJET_API UFpsArmsAnimInstance : public UAnimInstance
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
virtual void NativeInitializeAnimation() override;
|
||||||
|
virtual void NativeUpdateAnimation(float DeltaSeconds) override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// Etat publie vers le Blueprint d'animation
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
|
||||||
|
/** Vitesse horizontale en cm/s. Sert aussi a moduler le PlayRate de la course. */
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Movement")
|
||||||
|
float GroundSpeed = 0.f;
|
||||||
|
|
||||||
|
/** Vrai quand le joueur se deplace REELLEMENT, freinage exclu. */
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Movement")
|
||||||
|
bool bIsMoving = false;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Movement")
|
||||||
|
bool bIsSprinting = false;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Movement")
|
||||||
|
bool bIsFalling = false;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Movement")
|
||||||
|
bool bIsCrouching = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vitesse de lecture du cycle de deplacement, a brancher sur la broche
|
||||||
|
* Play Rate du noeud d'animation.
|
||||||
|
*
|
||||||
|
* Le projet a trois vitesses de jeu (accroupi 200, marche 400, sprint 650)
|
||||||
|
* pour un seul cycle d'animation. Plutot qu'un etat par vitesse -- trois
|
||||||
|
* seuils, donc trois occasions de claquer quand on les franchit -- on etire
|
||||||
|
* le meme cycle. La cadence des bras suit alors la vitesse en continu, y
|
||||||
|
* compris pendant l'acceleration.
|
||||||
|
*
|
||||||
|
* Un vrai etat Sprint n'aura de sens que le jour ou l'animateur livrera une
|
||||||
|
* animation de sprint a la POSTURE differente. Le meme cycle joue plus vite
|
||||||
|
* n'est pas une animation de sprint.
|
||||||
|
*/
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Movement")
|
||||||
|
float LocomotionPlayRate = 1.f;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* En dessous de cette vitesse (cm/s) on considere le joueur immobile.
|
||||||
|
*
|
||||||
|
* Un seuil strictement a zero ferait clignoter la transition course/repos
|
||||||
|
* pendant le freinage, ou la vitesse decroit en passant par des valeurs
|
||||||
|
* minuscules mais non nulles.
|
||||||
|
*/
|
||||||
|
UPROPERTY(EditDefaultsOnly, Category = "Movement", meta = (ClampMin = "0"))
|
||||||
|
float MovingSpeedThreshold = 10.f;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vitesse (cm/s) a laquelle le cycle tourne a sa cadence d'origine.
|
||||||
|
*
|
||||||
|
* A regler a l'oeil dans ABP_FpsArms : c'est la vitesse ou les bras ont l'air
|
||||||
|
* naturels sans etirement. Par defaut la vitesse de marche du pawn, donc le
|
||||||
|
* sprint accelere d'environ 60 %.
|
||||||
|
*/
|
||||||
|
UPROPERTY(EditDefaultsOnly, Category = "Movement", meta = (ClampMin = "1"))
|
||||||
|
float ReferenceLocomotionSpeed = 400.f;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bornes du Play Rate. Sans le plancher, un joueur qui rampe a 20 cm/s
|
||||||
|
* regarderait des bras quasi immobiles ; sans le plafond, une future vitesse
|
||||||
|
* bonus donnerait des bras en accelere qui sortent du registre credible.
|
||||||
|
*/
|
||||||
|
UPROPERTY(EditDefaultsOnly, Category = "Movement", meta = (ClampMin = "0.1"))
|
||||||
|
float MinLocomotionPlayRate = 0.6f;
|
||||||
|
|
||||||
|
UPROPERTY(EditDefaultsOnly, Category = "Movement", meta = (ClampMin = "0.1"))
|
||||||
|
float MaxLocomotionPlayRate = 1.8f;
|
||||||
|
|
||||||
|
private:
|
||||||
|
/**
|
||||||
|
* Cache du pawn. TryGetPawnOwner() fait un Cast a chaque appel, et cette
|
||||||
|
* fonction tourne a chaque frame sur un mesh anime.
|
||||||
|
*
|
||||||
|
* TWeakObjectPtr et pas un pointeur nu : apres un respawn le pawn est detruit,
|
||||||
|
* et cet objet peut lui survivre d'une frame.
|
||||||
|
*/
|
||||||
|
TWeakObjectPtr<AFpsPlayer> OwningPlayer;
|
||||||
|
};
|
||||||
@@ -7,8 +7,10 @@
|
|||||||
#include "FpsPlayer.generated.h"
|
#include "FpsPlayer.generated.h"
|
||||||
|
|
||||||
class APickupItem;
|
class APickupItem;
|
||||||
|
class UAnimMontage;
|
||||||
class UCameraComponent;
|
class UCameraComponent;
|
||||||
class UCraftingComponent;
|
class UCraftingComponent;
|
||||||
|
class UHeadBobComponent;
|
||||||
class UInputAction;
|
class UInputAction;
|
||||||
class UInputMappingContext;
|
class UInputMappingContext;
|
||||||
class UInteractionComponent;
|
class UInteractionComponent;
|
||||||
@@ -88,6 +90,26 @@ public:
|
|||||||
UFUNCTION(BlueprintPure, Category = "Camera")
|
UFUNCTION(BlueprintPure, Category = "Camera")
|
||||||
UCameraComponent* GetFirstPersonCamera() const { return FirstPersonCamera; }
|
UCameraComponent* GetFirstPersonCamera() const { return FirstPersonCamera; }
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintPure, Category = "Camera")
|
||||||
|
UHeadBobComponent* GetHeadBob() const { return HeadBob; }
|
||||||
|
|
||||||
|
/** Les bras vus par le joueur local. C'est ici que viendra l'objet en main. */
|
||||||
|
UFUNCTION(BlueprintPure, Category = "Animation")
|
||||||
|
USkeletalMeshComponent* GetFirstPersonArms() const { return FirstPersonArms; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Joue un montage sur les bras du joueur LOCAL, et seulement lui.
|
||||||
|
*
|
||||||
|
* Rien n'est replique et c'est voulu : les bras sont en OnlyOwnerSee, donc
|
||||||
|
* les animer chez le serveur ou chez un ami ferait tourner un squelette que
|
||||||
|
* personne ne rend. Le geste vu par les autres joueurs viendra du mesh 3e
|
||||||
|
* personne, avec ses propres animations, le jour ou l'animateur les livrera.
|
||||||
|
*
|
||||||
|
* @return la duree du montage en secondes, 0 s'il n'a pas pu etre joue.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Animation")
|
||||||
|
float PlayArmsMontage(UAnimMontage* Montage, float PlayRate = 1.f);
|
||||||
|
|
||||||
UFUNCTION(BlueprintPure, Category = "Interaction")
|
UFUNCTION(BlueprintPure, Category = "Interaction")
|
||||||
UInteractionComponent* GetInteractionComponent() const { return InteractionComponent; }
|
UInteractionComponent* GetInteractionComponent() const { return InteractionComponent; }
|
||||||
|
|
||||||
@@ -198,6 +220,40 @@ protected:
|
|||||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera")
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera")
|
||||||
TObjectPtr<UCameraComponent> FirstPersonCamera;
|
TObjectPtr<UCameraComponent> FirstPersonCamera;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bras vue première personne, visibles du seul propriétaire.
|
||||||
|
*
|
||||||
|
* Attachés à la CAMÉRA et non à la capsule : ils doivent suivre le tangage du
|
||||||
|
* regard, sinon lever les yeux les laisserait à l'horizontale. Bénéfice
|
||||||
|
* gratuit au passage — ils héritent du ressort d'impact et de la bascule
|
||||||
|
* d'accroupissement, donc les bras encaissent l'atterrissage avec la caméra
|
||||||
|
* sans une ligne de code de plus.
|
||||||
|
*
|
||||||
|
* Sa transform relative se règle À L'ŒIL dans BP_FpsPlayer : les valeurs du
|
||||||
|
* constructeur ne sont qu'un point de départ, elles dépendent entièrement de
|
||||||
|
* l'origine du FBX livré par l'animateur.
|
||||||
|
*/
|
||||||
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Animation")
|
||||||
|
TObjectPtr<USkeletalMeshComponent> FirstPersonArms;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Montage joué sur les bras au ramassage d'un objet (MTG_GrabItem).
|
||||||
|
* Vide = aucun geste, le ramassage fonctionne quand même.
|
||||||
|
*/
|
||||||
|
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Animation")
|
||||||
|
TObjectPtr<UAnimMontage> GrabMontage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Balancement de tête procédural : respiration à l'arrêt, pas à la marche,
|
||||||
|
* foulée au sprint. Tous ses réglages sont dans le Details panel du composant,
|
||||||
|
* pas ici : cinq profils de cinq valeurs noieraient la catégorie "Camera".
|
||||||
|
*
|
||||||
|
* Il ne touche pas la caméra lui-même, il expose deux décalages que
|
||||||
|
* ApplyCameraTransform additionne — cette fonction reste le seul écrivain.
|
||||||
|
*/
|
||||||
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera")
|
||||||
|
TObjectPtr<UHeadBobComponent> HeadBob;
|
||||||
|
|
||||||
/** Détecte et déclenche les interactions avec le monde. */
|
/** Détecte et déclenche les interactions avec le monde. */
|
||||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Interaction")
|
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Interaction")
|
||||||
TObjectPtr<UInteractionComponent> InteractionComponent;
|
TObjectPtr<UInteractionComponent> InteractionComponent;
|
||||||
|
|||||||
@@ -0,0 +1,234 @@
|
|||||||
|
// Fill out your copyright notice in the Description page of Project Settings.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "Components/ActorComponent.h"
|
||||||
|
#include "HeadBobComponent.generated.h"
|
||||||
|
|
||||||
|
class ACharacter;
|
||||||
|
class UCharacterMovementComponent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Les reglages d'un etat de deplacement : immobile, marche, sprint, accroupi, en l'air.
|
||||||
|
*
|
||||||
|
* Regroupes dans une struct plutot qu'a plat sur le composant pour deux raisons.
|
||||||
|
* D'abord le Details panel : cinq etats x cinq valeurs a plat feraient vingt-cinq
|
||||||
|
* lignes melangees, alors que la struct donne un repli par etat. Ensuite le code :
|
||||||
|
* passer d'un etat a l'autre est une interpolation champ a champ, et une struct
|
||||||
|
* permet de l'ecrire une seule fois au lieu d'une par valeur.
|
||||||
|
*/
|
||||||
|
USTRUCT(BlueprintType)
|
||||||
|
struct FHeadBobProfile
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
FHeadBobProfile() = default;
|
||||||
|
|
||||||
|
FHeadBobProfile(float InStrideFrequency, float InVerticalAmount, float InLateralAmount, float InRollAmount, float InPitchAmount)
|
||||||
|
: StrideFrequency(InStrideFrequency)
|
||||||
|
, VerticalAmount(InVerticalAmount)
|
||||||
|
, LateralAmount(InLateralAmount)
|
||||||
|
, RollAmount(InRollAmount)
|
||||||
|
, PitchAmount(InPitchAmount)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Foulees par seconde. Une foulee compte DEUX pas : l'oscillation verticale
|
||||||
|
* bat donc au double de cette valeur, une bosse par pied qui touche le sol.
|
||||||
|
* A 1.1, les pas tombent a 2.2 Hz, ce qui correspond a une marche normale.
|
||||||
|
*/
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Head Bob", meta = (ClampMin = "0", UIMax = "4"))
|
||||||
|
float StrideFrequency = 1.1f;
|
||||||
|
|
||||||
|
/** Amplitude verticale en cm, mesuree du repos au sommet (le debattement total vaut le double). */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Head Bob", meta = (ClampMin = "0", UIMax = "10"))
|
||||||
|
float VerticalAmount = 1.6f;
|
||||||
|
|
||||||
|
/** Amplitude laterale en cm : le balancement gauche-droite du buste. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Head Bob", meta = (ClampMin = "0", UIMax = "10"))
|
||||||
|
float LateralAmount = 1.2f;
|
||||||
|
|
||||||
|
/** Roulis en degres au pic du balancement lateral. La tete penche dans le sens du pas. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Head Bob", meta = (UIMin = "-3", UIMax = "3"))
|
||||||
|
float RollAmount = 0.4f;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tangage en degres au pic vertical. A 0 par defaut, et ce n'est pas un oubli :
|
||||||
|
* le tangage deplace le reticule, donc il degrade la visee. A n'ouvrir que si
|
||||||
|
* l'on assume un jeu ou l'on ne vise pas au pixel.
|
||||||
|
*/
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Head Bob", meta = (UIMin = "-3", UIMax = "3"))
|
||||||
|
float PitchAmount = 0.f;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Balancement de tete procedural, sans la moindre animation authoree.
|
||||||
|
*
|
||||||
|
* Le principe tient en une phase unique, avancee chaque frame par la frequence de
|
||||||
|
* l'etat courant, et une amplitude qui glisse d'un etat a l'autre. C'est ce qui
|
||||||
|
* rend le passage marche -> sprint continu : ce n'est pas une animation qu'on
|
||||||
|
* coupe pour en lancer une autre, ce sont deux jeux de coefficients entre lesquels
|
||||||
|
* on interpole pendant que la phase, elle, ne s'interrompt jamais. Remettre la
|
||||||
|
* phase a zero a chaque changement d'etat produirait exactement le "clac" qu'on
|
||||||
|
* cherche a eviter.
|
||||||
|
*
|
||||||
|
* Le composant ne touche PAS a la camera. Il expose un decalage de position et un
|
||||||
|
* decalage de rotation, que AFpsPlayer::ApplyCameraTransform additionne aux siens.
|
||||||
|
* C'est la condition pour que cette fonction reste le seul endroit du projet qui
|
||||||
|
* ecrit la transform de la camera -- elle est rappelee depuis CalcCamera, et deux
|
||||||
|
* ecrivains se marcheraient dessus une frame sur deux.
|
||||||
|
*
|
||||||
|
* Rien n'est replique et rien n'a a l'etre : le balancement n'existe que dans les
|
||||||
|
* yeux du joueur local. Les bras suivent gratuitement, puisqu'ils sont attaches a
|
||||||
|
* la camera.
|
||||||
|
*/
|
||||||
|
UCLASS(ClassGroup = (Survival), meta = (BlueprintSpawnableComponent))
|
||||||
|
class SURVIVAL_PROJET_API UHeadBobComponent : public UActorComponent
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UHeadBobComponent();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fait avancer la phase et recalcule les decalages.
|
||||||
|
*
|
||||||
|
* Appelee par le pawn depuis son Tick, et surtout PAS depuis un TickComponent a
|
||||||
|
* lui : l'ordre de tick entre un acteur et ses composants n'est pas garanti sans
|
||||||
|
* dependance explicite, et le resultat serait un decalage lu une frame apres
|
||||||
|
* avoir ete calcule -- invisible a 120 fps, franchement mou a 30.
|
||||||
|
*/
|
||||||
|
void UpdateBob(float DeltaTime);
|
||||||
|
|
||||||
|
/** Decalage a ajouter a la position RELATIVE de la camera (X avant, Y droite, Z haut), en cm. */
|
||||||
|
UFUNCTION(BlueprintPure, Category = "Head Bob")
|
||||||
|
FVector GetLocationOffset() const { return LocationOffset; }
|
||||||
|
|
||||||
|
/** Decalage a ajouter a la rotation de la camera, en degres. */
|
||||||
|
UFUNCTION(BlueprintPure, Category = "Head Bob")
|
||||||
|
FRotator GetRotationOffset() const { return RotationOffset; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remet le balancement au repos sans transition.
|
||||||
|
* Pour une teleportation ou une prise de controle : on ne veut pas que la tete
|
||||||
|
* finisse le pas d'avant a l'autre bout de la carte.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Head Bob")
|
||||||
|
void ResetBob();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Facteur global pousse par les reglages joueur (curseur "secousses de camera").
|
||||||
|
* A 0 la tete ne bouge plus du tout : c'est un confort d'accessibilite, pas une
|
||||||
|
* option cosmetique, et il doit valoir pour le balancement comme pour les chocs.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Head Bob")
|
||||||
|
void SetIntensityScale(float NewScale);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual void BeginPlay() override;
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// Reglages generaux
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
|
||||||
|
/** Coupe entierement l'effet, sans avoir a remettre les cinq profils a zero. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Head Bob")
|
||||||
|
bool bEnableHeadBob = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Multiplicateur d'amplitude commun aux cinq profils.
|
||||||
|
* C'est le curseur a bouger pour un reglage d'ensemble ("un peu moins de tout")
|
||||||
|
* sans avoir a retoucher vingt valeurs une par une.
|
||||||
|
*/
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Head Bob", meta = (ClampMin = "0", UIMax = "3"))
|
||||||
|
float GlobalIntensity = 1.f;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vitesse de fondu d'un profil vers un autre (facteur d'interpolation
|
||||||
|
* exponentielle, par seconde). Plus haut = le sprint prend son amplitude plus
|
||||||
|
* vite. Trop haut, le changement d'etat redevient une cassure.
|
||||||
|
*/
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Head Bob", meta = (ClampMin = "0.1", UIMax = "20"))
|
||||||
|
float ProfileBlendSpeed = 6.f;
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// Seuils de choix d'etat
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* En dessous de cette vitesse horizontale (cm/s), on est considere immobile.
|
||||||
|
* Le seuil se lit sur la vitesse REELLE et non sur la touche enfoncee : marcher
|
||||||
|
* dans un mur laisse l'input actif alors que le corps ne bouge plus, et sans ce
|
||||||
|
* detail la tete continuerait de gigoter face a la cloison.
|
||||||
|
*/
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Head Bob|Seuils", meta = (ClampMin = "0"))
|
||||||
|
float IdleSpeedThreshold = 10.f;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Au-dessus de cette vitesse horizontale (cm/s), on bascule sur le profil de
|
||||||
|
* sprint. A regler entre WalkSpeed et SprintSpeed du pawn -- 400 et 650 par
|
||||||
|
* defaut, d'ou ces 520 : assez haut pour que la phase d'acceleration ne compte
|
||||||
|
* pas comme du sprint, assez bas pour que le sprint reel soit toujours detecte.
|
||||||
|
*/
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Head Bob|Seuils", meta = (ClampMin = "0"))
|
||||||
|
float SprintSpeedThreshold = 520.f;
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// Profils, un par etat. Valeurs par defaut dans le constructeur.
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
|
||||||
|
/** Immobile : une respiration lente, quasi purement verticale. C'est ce qui evite la statue. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Head Bob|Profils")
|
||||||
|
FHeadBobProfile IdleProfile;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Head Bob|Profils")
|
||||||
|
FHeadBobProfile WalkProfile;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Head Bob|Profils")
|
||||||
|
FHeadBobProfile SprintProfile;
|
||||||
|
|
||||||
|
/** Accroupi : plus lent et plus discret que la marche, le corps est ramasse. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Head Bob|Profils")
|
||||||
|
FHeadBobProfile CrouchProfile;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* En l'air : tout a zero, y compris la frequence -- la phase se fige donc en vol
|
||||||
|
* et repart d'ou elle en etait a la reception, ce qui evite que le premier pas
|
||||||
|
* apres un saut tombe sur un pied arbitraire. Le profil reste editable malgre
|
||||||
|
* tout, au cas ou l'on veuille un flottement pendant la chute.
|
||||||
|
*/
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Head Bob|Profils")
|
||||||
|
FHeadBobProfile AirProfile;
|
||||||
|
|
||||||
|
private:
|
||||||
|
/** Choisit le profil vise a partir de l'etat reel du CharacterMovement. */
|
||||||
|
const FHeadBobProfile& SelectTargetProfile() const;
|
||||||
|
|
||||||
|
/** Rapproche BlendedProfile de Target, champ par champ. */
|
||||||
|
void BlendTowards(const FHeadBobProfile& Target, float Step);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Position dans le cycle de foulee, en radians, ramenee dans [0, 2*PI[.
|
||||||
|
* Le repli n'est pas cosmetique : sans lui la valeur grossit indefiniment et la
|
||||||
|
* precision du float se degrade, jusqu'a un balancement qui devient saccade au
|
||||||
|
* bout d'une longue partie.
|
||||||
|
*/
|
||||||
|
float StridePhase = 0.f;
|
||||||
|
|
||||||
|
/** Profil courant, resultat du fondu entre les profils d'etat. */
|
||||||
|
FHeadBobProfile BlendedProfile;
|
||||||
|
|
||||||
|
FVector LocationOffset = FVector::ZeroVector;
|
||||||
|
|
||||||
|
FRotator RotationOffset = FRotator::ZeroRotator;
|
||||||
|
|
||||||
|
/** Pousse par les reglages joueur, se multiplie a GlobalIntensity. */
|
||||||
|
float IntensityScale = 1.f;
|
||||||
|
|
||||||
|
/** Caches : ces deux pointeurs sont lus a chaque frame, pas question d'y caster. */
|
||||||
|
TObjectPtr<ACharacter> OwnerCharacter;
|
||||||
|
|
||||||
|
TObjectPtr<UCharacterMovementComponent> OwnerMovement;
|
||||||
|
};
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
// Fill out your copyright notice in the Description page of Project Settings.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "InputCoreTypes.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Traduction d'une touche de reference vers la disposition clavier du systeme.
|
||||||
|
*
|
||||||
|
* LE PROBLEME. Un IMC stocke des FKey -- EKeys::W --, c'est-a-dire des LETTRES.
|
||||||
|
* Or Windows livre au moteur un virtual key code deja traduit par la disposition
|
||||||
|
* active : sur un clavier AZERTY, la touche en haut a gauche du bloc de
|
||||||
|
* deplacement envoie VK_Z, qu'Unreal lit donc comme EKeys::Z. Le mapping "W" ne
|
||||||
|
* se declenche jamais, et le joueur pousse son personnage en arriere en croyant
|
||||||
|
* avancer. Rien de tout ca n'est visible en developpant sur un clavier QWERTY.
|
||||||
|
*
|
||||||
|
* LA REPONSE. On ne raisonne plus en lettres mais en POSITIONS. Chaque touche
|
||||||
|
* par defaut de l'IMC est lue comme une position physique sur un clavier QWERTY
|
||||||
|
* US -- c'est le referentiel dans lequel les IMC sont ecrits -- et l'on demande
|
||||||
|
* a l'OS quelle touche occupe cette position sur la disposition courante. La
|
||||||
|
* geometrie de la main est preservee : le bloc WASD devient ZQSD en AZERTY,
|
||||||
|
* reste WASD en QWERTZ (seuls Y et Z y sont echanges), et suit les positions en
|
||||||
|
* Dvorak ou Colemak. Aucune table par pays a maintenir, et les dispositions
|
||||||
|
* qu'on n'a jamais vues sont traitees comme les autres.
|
||||||
|
*
|
||||||
|
* PORTEE. Seule la zone alphanumerique bouge d'une disposition a l'autre. Les
|
||||||
|
* modificateurs, la barre d'espace, Echap, les fonctions, les fleches, le pave
|
||||||
|
* numerique et la manette occupent la meme position partout : ils ne figurent
|
||||||
|
* pas dans la table de reference et ressortent donc inchanges. C'est aussi ce
|
||||||
|
* qui garantit qu'on ne casse rien -- une touche inconnue est rendue telle quelle.
|
||||||
|
*
|
||||||
|
* WINDOWS UNIQUEMENT pour l'instant. Sur les autres plateformes la touche est
|
||||||
|
* rendue inchangee, soit exactement le comportement d'avant.
|
||||||
|
*/
|
||||||
|
namespace KeyboardLayout
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Rend la touche qui occupe, sur la disposition active, la position que
|
||||||
|
* ReferenceKey occupe sur un clavier QWERTY US.
|
||||||
|
*
|
||||||
|
* Rend ReferenceKey inchangee si la touche n'est pas concernee par la
|
||||||
|
* disposition (espace, modificateurs, manette...), si la plateforme n'est pas
|
||||||
|
* geree, ou si l'OS ne sait pas repondre. Jamais de touche invalide en sortie.
|
||||||
|
*/
|
||||||
|
SURVIVAL_PROJET_API FKey RemapKeyToActiveLayout(const FKey& ReferenceKey);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* L'identifiant de la disposition active, pour le journal : "0000040C" pour
|
||||||
|
* le francais, "00000409" pour l'anglais US. Sert au diagnostic quand un
|
||||||
|
* testeur signale des touches inattendues -- c'est la premiere chose a lire.
|
||||||
|
*/
|
||||||
|
SURVIVAL_PROJET_API FString GetActiveLayoutId();
|
||||||
|
}
|
||||||
@@ -114,7 +114,15 @@ public:
|
|||||||
UFUNCTION(BlueprintPure, Category = "Musique")
|
UFUNCTION(BlueprintPure, Category = "Musique")
|
||||||
EMusicLevelChangeMode GetLevelChangeMode() const { return LevelChangeMode; }
|
EMusicLevelChangeMode GetLevelChangeMode() const { return LevelChangeMode; }
|
||||||
|
|
||||||
/** Volume joueur, 0 a 1. A brancher sur l'onglet Audio des reglages plus tard. */
|
/**
|
||||||
|
* Attenuation propre au LECTEUR, 0 a 1. Ce n'est PAS le reglage du joueur.
|
||||||
|
*
|
||||||
|
* Le volume choisi dans les reglages passe par la SoundClass de musique (voir
|
||||||
|
* USurvivalAudioSettings), pas par ici : deux chemins vers le meme volume
|
||||||
|
* finiraient par se multiplier sans que personne ne s'en souvienne. Celui-ci
|
||||||
|
* reste disponible pour ce qui est de l'ordre du gameplay -- baisser la musique
|
||||||
|
* pendant un dialogue, par exemple.
|
||||||
|
*/
|
||||||
UFUNCTION(BlueprintCallable, Category = "Musique")
|
UFUNCTION(BlueprintCallable, Category = "Musique")
|
||||||
void SetMusicVolume(float NewVolume);
|
void SetMusicVolume(float NewVolume);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
// Fill out your copyright notice in the Description page of Project Settings.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "SettingsTabWidget.h"
|
||||||
|
#include "SettingsAudioWidget.generated.h"
|
||||||
|
|
||||||
|
class USettingRowWidget;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* L'onglet "Audio" : quatre volumes et le son en arriere-plan.
|
||||||
|
*
|
||||||
|
* Meme forme que USettingsGameplayWidget -- l'onglet ne stocke rien, chaque ligne
|
||||||
|
* ecrit dans USurvivalUserSettings puis demande l'application immediate. La
|
||||||
|
* difference tient au verbe appele : ApplyAudioSettings et non
|
||||||
|
* ApplySurvivalSettings, parce que les volumes n'interessent pas le pawn et que
|
||||||
|
* rediffuser aux widgets a chaque cran de curseur ne servirait a rien.
|
||||||
|
*
|
||||||
|
* Classe Abstract : on cree un WBP_SettingsAudio qui en herite, avec un
|
||||||
|
* WBP_SettingRow par ligne, nomme exactement comme le BindWidget correspondant.
|
||||||
|
*/
|
||||||
|
UCLASS(Abstract)
|
||||||
|
class SURVIVAL_PROJET_API USettingsAudioWidget : public USettingsTabWidget
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
virtual void NativeConstruct() override;
|
||||||
|
|
||||||
|
virtual void RefreshFromSettings() override;
|
||||||
|
virtual void ResetToDefaults() override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
UPROPERTY(meta = (BindWidget))
|
||||||
|
TObjectPtr<USettingRowWidget> MasterVolumeRow;
|
||||||
|
|
||||||
|
UPROPERTY(meta = (BindWidget))
|
||||||
|
TObjectPtr<USettingRowWidget> MusicVolumeRow;
|
||||||
|
|
||||||
|
UPROPERTY(meta = (BindWidget))
|
||||||
|
TObjectPtr<USettingRowWidget> EffectsVolumeRow;
|
||||||
|
|
||||||
|
UPROPERTY(meta = (BindWidget))
|
||||||
|
TObjectPtr<USettingRowWidget> UiVolumeRow;
|
||||||
|
|
||||||
|
UPROPERTY(meta = (BindWidget))
|
||||||
|
TObjectPtr<USettingRowWidget> BackgroundSoundRow;
|
||||||
|
|
||||||
|
private:
|
||||||
|
UFUNCTION()
|
||||||
|
void HandleMasterVolumeChanged(int32 NewIndex);
|
||||||
|
|
||||||
|
UFUNCTION()
|
||||||
|
void HandleMusicVolumeChanged(int32 NewIndex);
|
||||||
|
|
||||||
|
UFUNCTION()
|
||||||
|
void HandleEffectsVolumeChanged(int32 NewIndex);
|
||||||
|
|
||||||
|
UFUNCTION()
|
||||||
|
void HandleUiVolumeChanged(int32 NewIndex);
|
||||||
|
|
||||||
|
UFUNCTION()
|
||||||
|
void HandleBackgroundSoundChanged(int32 NewIndex);
|
||||||
|
};
|
||||||
@@ -124,8 +124,7 @@ protected:
|
|||||||
TObjectPtr<UButton> ResetButton;
|
TObjectPtr<UButton> ResetButton;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Les trois onglets qui existent sont OBLIGATOIRES : un WBP_Settings sans eux
|
* Les cinq onglets sont OBLIGATOIRES : un WBP_Settings sans eux ne compile pas.
|
||||||
* ne compile pas.
|
|
||||||
*
|
*
|
||||||
* BindWidgetOptional echoue en silence -- un nom mal orthographie donnait un
|
* BindWidgetOptional echoue en silence -- un nom mal orthographie donnait un
|
||||||
* bouton parfaitement inerte, sans le moindre message. Pour un element dont
|
* bouton parfaitement inerte, sans le moindre message. Pour un element dont
|
||||||
@@ -143,12 +142,7 @@ protected:
|
|||||||
UPROPERTY(meta = (BindWidget))
|
UPROPERTY(meta = (BindWidget))
|
||||||
TObjectPtr<UButton> ControlsTabButton;
|
TObjectPtr<UButton> ControlsTabButton;
|
||||||
|
|
||||||
/**
|
UPROPERTY(meta = (BindWidget))
|
||||||
* Celui-la reste optionnel : sa page n'existe pas encore. Il passera en
|
|
||||||
* BindWidget le jour ou l'onglet Audio sera ecrit -- en attendant, il se
|
|
||||||
* grise tout seul si tu le poses.
|
|
||||||
*/
|
|
||||||
UPROPERTY(meta = (BindWidgetOptional))
|
|
||||||
TObjectPtr<UButton> AudioTabButton;
|
TObjectPtr<UButton> AudioTabButton;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
// Fill out your copyright notice in the Description page of Project Settings.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "Engine/DeveloperSettings.h"
|
||||||
|
// Sound/SoundClass.h en entier, et pas une simple declaration anticipee :
|
||||||
|
// TStrongObjectPtr exige un type COMPLET, la ou TObjectPtr s'en passait.
|
||||||
|
#include "Sound/SoundClass.h"
|
||||||
|
#include "UObject/StrongObjectPtr.h"
|
||||||
|
#include "SurvivalAudioSettings.generated.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ou le C++ trouve les SoundClass du jeu. Reglable dans Project Settings, section
|
||||||
|
* "Survival Audio".
|
||||||
|
*
|
||||||
|
* UDeveloperSettings et pas des chemins codes en dur dans le .cpp : renommer ou
|
||||||
|
* deplacer SC_Music casserait silencieusement le curseur Musique, sans la moindre
|
||||||
|
* erreur de compilation. Ici la reference est un asset comme un autre, suivie par
|
||||||
|
* le redirecteur de l'editeur et par le cooker.
|
||||||
|
*
|
||||||
|
* Ces references sont SOFT : un UDeveloperSettings est construit tres tot, bien
|
||||||
|
* avant que le gestionnaire d'assets ne soit pret. Une reference dure forcerait le
|
||||||
|
* chargement des SoundClass au demarrage du moteur, ce qui est exactement le genre
|
||||||
|
* de dependance qui fait echouer un cook.
|
||||||
|
*
|
||||||
|
* A NOTER : le SoundMix, lui, n'est PAS ici. Il est deja declare dans
|
||||||
|
* Project Settings > Audio > Default Base Sound Mix, d'ou le moteur le reactive a
|
||||||
|
* chaque chargement de map -- c'est precisement ce qu'on veut, et le redeclarer ici
|
||||||
|
* ouvrirait la porte a deux references qui divergent.
|
||||||
|
*/
|
||||||
|
UCLASS(config = Game, defaultconfig, meta = (DisplayName = "Survival Audio"))
|
||||||
|
class SURVIVAL_PROJET_API USurvivalAudioSettings : public UDeveloperSettings
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
/** Range la page sous "Game" et non sous "Plugins". */
|
||||||
|
virtual FName GetCategoryName() const override;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Le CDO, seul exemplaire qui existe.
|
||||||
|
*
|
||||||
|
* Non const alors que rien n'est modifie du point de vue du joueur : les
|
||||||
|
* accesseurs ci-dessous memorisent l'asset resolu dans un champ Transient, comme
|
||||||
|
* le fait UAudioSettings pour sa propre SoundClass par defaut.
|
||||||
|
*/
|
||||||
|
static USurvivalAudioSettings* Get();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Le sommet de la hierarchie : toutes les autres classes en descendent, donc son
|
||||||
|
* volume les multiplie toutes.
|
||||||
|
*/
|
||||||
|
UPROPERTY(config, EditAnywhere, Category = "Sound Classes")
|
||||||
|
TSoftObjectPtr<USoundClass> MasterSoundClass;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Imposee au lecteur de musique par UMusicSubsystem, donc les morceaux eux-memes
|
||||||
|
* n'ont rien a declarer -- ajouter une piste a une playlist suffit.
|
||||||
|
*/
|
||||||
|
UPROPERTY(config, EditAnywhere, Category = "Sound Classes")
|
||||||
|
TSoftObjectPtr<USoundClass> MusicSoundClass;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A declarer aussi comme Default Sound Class dans Project Settings > Audio :
|
||||||
|
* tout son qui n'annonce pas sa classe atterrit alors ici, ce qui evite qu'un
|
||||||
|
* bruitage oublie echappe au curseur Effets.
|
||||||
|
*/
|
||||||
|
UPROPERTY(config, EditAnywhere, Category = "Sound Classes")
|
||||||
|
TSoftObjectPtr<USoundClass> EffectsSoundClass;
|
||||||
|
|
||||||
|
/** A poser a la main sur les sons d'interface (survol, validation). */
|
||||||
|
UPROPERTY(config, EditAnywhere, Category = "Sound Classes")
|
||||||
|
TSoftObjectPtr<USoundClass> UiSoundClass;
|
||||||
|
|
||||||
|
USoundClass* GetMasterSoundClass();
|
||||||
|
USoundClass* GetMusicSoundClass();
|
||||||
|
USoundClass* GetEffectsSoundClass();
|
||||||
|
USoundClass* GetUiSoundClass();
|
||||||
|
|
||||||
|
private:
|
||||||
|
/**
|
||||||
|
* Charge au premier appel, puis rend le pointeur memorise.
|
||||||
|
*
|
||||||
|
* Le cache n'est pas qu'une optimisation : il fait aussi office d'ancre pour le
|
||||||
|
* ramasse-miettes. Une SoundClass chargee par un LoadSynchronous et referencee
|
||||||
|
* nulle part serait collectee, et le curseur cesserait d'agir en pleine partie.
|
||||||
|
*/
|
||||||
|
USoundClass* Resolve(const TSoftObjectPtr<USoundClass>& Asset, TStrongObjectPtr<USoundClass>& Cache);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TStrongObjectPtr et surtout PAS UPROPERTY(Transient) : c'est ce qui plantait
|
||||||
|
* le jeu package au demarrage, alors que tout allait bien dans l'editeur.
|
||||||
|
*
|
||||||
|
* Le CDO d'un UDeveloperSettings naît avant CloseDisregardForGC, donc il vit dans
|
||||||
|
* le pool des objets PERMANENTS. Le moteur ne les scanne jamais -- ils sont
|
||||||
|
* immortels par construction -- et exige en echange qu'ils ne referencent que des
|
||||||
|
* objets du root set, sans quoi ils garderaient un pointeur sur un objet collecte.
|
||||||
|
* VerifyGCAssumptions le controle et c'est une erreur FATALE :
|
||||||
|
*
|
||||||
|
* Disregard for GC object Default__SurvivalAudioSettings referencing SoundClass
|
||||||
|
* SC_Master which is not part of root set. Property: CachedMasterSoundClass
|
||||||
|
* Fatal error: Encountered 3 object(s) breaking Disregard for GC assumptions.
|
||||||
|
*
|
||||||
|
* Invisible dans l'editeur, ou les SoundClass sont deja chargees quand le pool se
|
||||||
|
* ferme, donc permanentes elles aussi. En jeu package elles arrivent apres, et le
|
||||||
|
* jeu meurt au premier ramassage -- pendant le chargement de la premiere map.
|
||||||
|
*
|
||||||
|
* TStrongObjectPtr ancre l'asset exactement comme le faisait la UPROPERTY, mais
|
||||||
|
* range sa reference dans GGCObjectReferencer, le seul objet que VerifyGCAssumptions
|
||||||
|
* saute explicitement. Le CDO ne reference donc plus rien, et l'asset reste vivant.
|
||||||
|
*/
|
||||||
|
TStrongObjectPtr<USoundClass> CachedMasterSoundClass;
|
||||||
|
|
||||||
|
TStrongObjectPtr<USoundClass> CachedMusicSoundClass;
|
||||||
|
|
||||||
|
TStrongObjectPtr<USoundClass> CachedEffectsSoundClass;
|
||||||
|
|
||||||
|
TStrongObjectPtr<USoundClass> CachedUiSoundClass;
|
||||||
|
};
|
||||||
@@ -51,6 +51,31 @@ public:
|
|||||||
UFUNCTION(BlueprintCallable, Category = "Input")
|
UFUNCTION(BlueprintCallable, Category = "Input")
|
||||||
void RegisterMappableContexts();
|
void RegisterMappableContexts();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Traduit les touches par defaut vers la disposition clavier du joueur.
|
||||||
|
*
|
||||||
|
* Les IMC sont ecrits en QWERTY US -- EKeys::W pour avancer -- et Windows livre
|
||||||
|
* au moteur une touche DEJA traduite par la disposition : en AZERTY, la touche
|
||||||
|
* du haut du bloc de deplacement arrive comme EKeys::Z et le mapping "W" ne se
|
||||||
|
* declenche jamais. On repose donc chaque touche a la position equivalente.
|
||||||
|
* Le detail du mecanisme est dans KeyboardLayout.h.
|
||||||
|
*
|
||||||
|
* Ne touche QUE les mappings que le joueur n'a pas remappes lui-meme : un
|
||||||
|
* choix explicite l'emporte toujours sur une deduction. Et sur un clavier
|
||||||
|
* QWERTY la conversion rend la touche d'origine, donc rien n'est ecrit du tout
|
||||||
|
* et le profil reste vierge.
|
||||||
|
*
|
||||||
|
* Declenchee UNE FOIS par RegisterMappableContexts(), au premier enregistrement
|
||||||
|
* des contextes ; ensuite seul "Reinitialiser" la redemande. Elle appartient a
|
||||||
|
* l'initialisation, pas au rafraichissement d'ecran -- l'onglet Touches se
|
||||||
|
* rafraichit apres chaque remappage, et repasser dessus reprendrait sa touche
|
||||||
|
* au joueur qui aurait justement choisi celle de l'IMC (la lettre W sur un
|
||||||
|
* AZERTY, par exemple : identique au defaut, donc indiscernable d'un mapping
|
||||||
|
* jamais touche).
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Input")
|
||||||
|
void ApplyKeyboardLayoutToDefaultKeys();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Un depart VOULU vers le menu commence : le filet de secours reseau doit
|
* Un depart VOULU vers le menu commence : le filet de secours reseau doit
|
||||||
* se taire.
|
* se taire.
|
||||||
@@ -208,6 +233,16 @@ private:
|
|||||||
/** Un retour au menu est deja en cours, voulu ou non. */
|
/** Un retour au menu est deja en cours, voulu ou non. */
|
||||||
bool bReturningToMainMenu = false;
|
bool bReturningToMainMenu = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* La disposition clavier a deja ete appliquee dans cette session.
|
||||||
|
*
|
||||||
|
* Pas de UPROPERTY(config) : ce n'est PAS "l'a-t-on deja fait un jour". Les
|
||||||
|
* surcharges posees, elles, sont persistees par Enhanced Input, et elles
|
||||||
|
* suffisent a rendre l'operation sans effet au lancement suivant. Ce drapeau
|
||||||
|
* ne protege que du re-declenchement en cours de session.
|
||||||
|
*/
|
||||||
|
bool bKeyboardLayoutApplied = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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
|
||||||
* supprime une camera dans la map, et on retomberait sur autre chose.
|
* supprime une camera dans la map, et on retomberait sur autre chose.
|
||||||
|
|||||||
@@ -74,6 +74,23 @@ namespace SurvivalSettingsRange
|
|||||||
*/
|
*/
|
||||||
constexpr float LookMultiplierAtMin = 0.05f;
|
constexpr float LookMultiplierAtMin = 0.05f;
|
||||||
constexpr float LookMultiplierAtMax = 1.2f;
|
constexpr float LookMultiplierAtMax = 1.2f;
|
||||||
|
|
||||||
|
/** Les quatre volumes partagent la meme echelle : 0 a 100 %. */
|
||||||
|
constexpr float VolumeMin = 0.f;
|
||||||
|
constexpr float VolumeMax = 100.f;
|
||||||
|
constexpr float VolumeStep = 1.f;
|
||||||
|
constexpr float VolumeArrowStep = 10.f;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exposant applique au pourcentage avant d'en faire une amplitude.
|
||||||
|
*
|
||||||
|
* L'oreille repond au logarithme de l'amplitude, pas a l'amplitude : un
|
||||||
|
* curseur envoye tel quel donne un reglage inutilisable, ou tout se joue dans
|
||||||
|
* les vingt derniers pourcents et ou la moitie basse ne s'entend deja plus.
|
||||||
|
* Elever au carre est l'approximation habituelle -- 50 % rendent 0,25 en
|
||||||
|
* amplitude, soit environ la moitie du volume PERCU. 0 reste le silence exact.
|
||||||
|
*/
|
||||||
|
constexpr float VolumeCurveExponent = 2.f;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -128,10 +145,25 @@ public:
|
|||||||
UFUNCTION(BlueprintCallable, Category = "Reglages")
|
UFUNCTION(BlueprintCallable, Category = "Reglages")
|
||||||
void ApplySurvivalSettings();
|
void ApplySurvivalSettings();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pousse les volumes sur le peripherique audio.
|
||||||
|
*
|
||||||
|
* Volontairement SEPAREE d'ApplySurvivalSettings : cinq commandes vers le
|
||||||
|
* thread audio a chaque clic de fleche sur le champ de vision seraient du
|
||||||
|
* bruit pur. Les deux sont appelees ensemble par ApplyNonResolutionSettings,
|
||||||
|
* donc le chemin de demarrage n'en oublie aucune.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Reglages|Audio")
|
||||||
|
void ApplyAudioSettings();
|
||||||
|
|
||||||
/** Remet les seuls reglages de jeu a leur valeur d'usine. */
|
/** Remet les seuls reglages de jeu a leur valeur d'usine. */
|
||||||
UFUNCTION(BlueprintCallable, Category = "Reglages")
|
UFUNCTION(BlueprintCallable, Category = "Reglages")
|
||||||
void SetSurvivalGameplayToDefaults();
|
void SetSurvivalGameplayToDefaults();
|
||||||
|
|
||||||
|
/** Remet a l'usine les seuls reglages de l'onglet Audio. */
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Reglages|Audio")
|
||||||
|
void SetSurvivalAudioToDefaults();
|
||||||
|
|
||||||
virtual void SetToDefaults() override;
|
virtual void SetToDefaults() override;
|
||||||
virtual void ApplyNonResolutionSettings() override;
|
virtual void ApplyNonResolutionSettings() override;
|
||||||
|
|
||||||
@@ -215,6 +247,45 @@ public:
|
|||||||
UFUNCTION(BlueprintCallable, Category = "Reglages|Affichage")
|
UFUNCTION(BlueprintCallable, Category = "Reglages|Affichage")
|
||||||
void SetSurvivalDisplayToDefaults();
|
void SetSurvivalDisplayToDefaults();
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// Audio
|
||||||
|
//
|
||||||
|
// Les quatre volumes sont des POURCENTAGES, comme le bouge de camera : c'est
|
||||||
|
// ce que la ligne de reglage affiche, et la conversion en amplitude (courbe
|
||||||
|
// comprise) n'appartient qu'a ApplyAudioSettings.
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintPure, Category = "Reglages|Audio")
|
||||||
|
float GetMasterVolume() const { return MasterVolume; }
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Reglages|Audio")
|
||||||
|
void SetMasterVolume(float Value);
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintPure, Category = "Reglages|Audio")
|
||||||
|
float GetMusicVolume() const { return MusicVolume; }
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Reglages|Audio")
|
||||||
|
void SetMusicVolume(float Value);
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintPure, Category = "Reglages|Audio")
|
||||||
|
float GetEffectsVolume() const { return EffectsVolume; }
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Reglages|Audio")
|
||||||
|
void SetEffectsVolume(float Value);
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintPure, Category = "Reglages|Audio")
|
||||||
|
float GetUiVolume() const { return UiVolume; }
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Reglages|Audio")
|
||||||
|
void SetUiVolume(float Value);
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintPure, Category = "Reglages|Audio")
|
||||||
|
bool GetPlayInBackground() const { return bPlayInBackground; }
|
||||||
|
|
||||||
|
/** Vrai = le jeu continue de s'entendre quand la fenetre n'a plus le focus. */
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Reglages|Audio")
|
||||||
|
void SetPlayInBackground(bool bValue);
|
||||||
|
|
||||||
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)
|
||||||
@@ -245,4 +316,24 @@ private:
|
|||||||
/** Pourcentage, converti en gamma a l'application. */
|
/** Pourcentage, converti en gamma a l'application. */
|
||||||
UPROPERTY(config)
|
UPROPERTY(config)
|
||||||
float Brightness;
|
float Brightness;
|
||||||
|
|
||||||
|
/** Pourcentages. Le maitre multiplie les trois autres via la hierarchie des SoundClass. */
|
||||||
|
UPROPERTY(config)
|
||||||
|
float MasterVolume;
|
||||||
|
|
||||||
|
UPROPERTY(config)
|
||||||
|
float MusicVolume;
|
||||||
|
|
||||||
|
UPROPERTY(config)
|
||||||
|
float EffectsVolume;
|
||||||
|
|
||||||
|
UPROPERTY(config)
|
||||||
|
float UiVolume;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Le moteur coupe le son des qu'il perd le focus, et c'est son defaut d'usine.
|
||||||
|
* En coop c'est justement le mauvais defaut pour qui bascule sur Discord.
|
||||||
|
*/
|
||||||
|
UPROPERTY(config)
|
||||||
|
bool bPlayInBackground;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,7 +15,10 @@ public class Survival_projet : ModuleRules
|
|||||||
// extinction). Passer par un evenement Blueprint marcherait aussi, mais
|
// extinction). Passer par un evenement Blueprint marcherait aussi, mais
|
||||||
// l'etat du feu appartient au C++ -- le Blueprint ne doit brancher que
|
// l'etat du feu appartient au C++ -- le Blueprint ne doit brancher que
|
||||||
// l'asset, pas decider quand il s'allume.
|
// l'asset, pas decider quand il s'allume.
|
||||||
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "EnhancedInput", "GameplayTags", "UMG", "Niagara" });
|
// DeveloperSettings : USurvivalAudioSettings derive de UDeveloperSettings dans
|
||||||
|
// un header public, donc la dependance doit l'etre aussi. C'est ce qui donne
|
||||||
|
// la page "Survival Audio" dans les Project Settings.
|
||||||
|
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "EnhancedInput", "GameplayTags", "UMG", "Niagara", "DeveloperSettings" });
|
||||||
|
|
||||||
// EngineSettings : UGameMapsSettings::GetGameDefaultMap(), qui evite de
|
// EngineSettings : UGameMapsSettings::GetGameDefaultMap(), qui evite de
|
||||||
// redeclarer le chemin de la map de menu dans le C++ alors qu'il est
|
// redeclarer le chemin de la map de menu dans le C++ alors qu'il est
|
||||||
|
|||||||
Reference in New Issue
Block a user