Files
Unreal_EmberWild/Source/Survival_projet/Private/SurvivalUserSettings.cpp
T
Mathew 5970db0512 (Feat) Customisation -- replication et persistance
Lot 3 : l'apparence survit desormais au respawn, au changement de map et au
relancement du jeu, et les trois autres joueurs la voient.

Elle vit sur ASurvivalPlayerState et pas sur le pawn : c'est le JOUEUR
qu'elle decrit, pas le corps. Le pawn meurt et reapparait, le PlayerState
non -- reapparaitre avec le visage de quelqu'un d'autre n'aurait aucun sens,
alors que perdre son inventaire au sol est la regle du jeu.

SubmitCharacterAppearance() sur AFpsPlayerController est le point d'entree
UNIQUE : il persiste ET annonce au serveur, jamais l'un sans l'autre.
Persister sans annoncer laisserait le joueur seul a se voir change, annoncer
sans persister le ferait repartir au visage par defaut au lancement suivant.
C'est par la que passent les commandes console, et par la que passera
l'ecran du lot 4.

La course pawn / PlayerState est resolue des DEUX cotes. Chez un client ils
arrivent par deux chemins de replication independants et rien ne dit lequel
gagne : OnRep_CharacterAppearance monte le personnage si le pawn est deja la,
AFpsPlayer::OnRep_PlayerState va chercher l'apparence si c'est l'inverse. Les
deux appellent la meme fonction idempotente. Chez l'hote aucun OnRep ne se
declenche, d'ou l'appel local dans SetCharacterAppearance -- sans lui, l'hote
serait le seul a ne pas voir son propre personnage.

Rien n'est valide cote PlayerState, deliberement : borner un index demande le
catalogue, que seul UCharacterAppearanceComponent connait, et celui-ci passe
deja tout ce qu'il monte par Catalog->Sanitize() sur chaque machine.

Piege de test note dans CLAUDE.md : en PIE a deux joueurs, les deux fenetres
lisent le meme .ini, donc les deux personnages demarrent identiques.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 19:45:01 +02:00

352 lines
11 KiB
C++

// Fill out your copyright notice in the Description page of Project Settings.
#include "SurvivalUserSettings.h"
#include "AudioDevice.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
{
/** Arrondit sur le palier le plus proche, puis borne. */
float SnapToStep(float Value, float Min, float Max, float Step)
{
const float Snapped = Min + FMath::RoundToFloat((Value - Min) / Step) * Step;
return FMath::Clamp(Snapped, Min, Max);
}
/** 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()
{
// Le constructeur tourne sur le CDO, avant toute lecture du .ini : ces
// valeurs sont donc bien les defauts d'usine, ecrasees ensuite par le
// fichier s'il existe.
SetSurvivalGameplayToDefaults();
SetSurvivalDisplayToDefaults();
SetSurvivalAudioToDefaults();
}
USurvivalUserSettings* USurvivalUserSettings::Get()
{
return GEngine ? Cast<USurvivalUserSettings>(GEngine->GetGameUserSettings()) : nullptr;
}
void USurvivalUserSettings::SetSurvivalGameplayToDefaults()
{
LookSensitivity = 35.f;
bSeparateVerticalSensitivity = false;
LookSensitivityVertical = 35.f;
bInvertLookY = false;
FieldOfView = 90.f;
CameraShakeScale = 100.f;
bSprintToggle = false;
bCrouchToggle = false;
}
void USurvivalUserSettings::SetSurvivalDisplayToDefaults()
{
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()
{
// Le "Reinitialiser" global du moteur doit aussi remettre NOS valeurs :
// sans cette surcharge, elles resteraient seules inchangees.
Super::SetToDefaults();
SetSurvivalGameplayToDefaults();
SetSurvivalDisplayToDefaults();
SetSurvivalAudioToDefaults();
}
void USurvivalUserSettings::SetBrightness(float Value)
{
using namespace SurvivalSettingsRange;
Brightness = SnapToStep(Value, BrightnessMin, BrightnessMax, BrightnessStep);
}
void USurvivalUserSettings::ApplySurvivalSettings()
{
// Le gamma n'appartient pas a UGameUserSettings : il vit sur GEngine, donc
// sur un objet GLOBAL au processus. Deux consequences :
//
// 1. DisplayGamma est un EXPOSANT dont la valeur moteur est 2.2, pas un
// multiplicateur neutre a 1. On module la reference, on ne la remplace
// pas.
// 2. En editeur, cet objet est celui de l'editeur : y toucher assombrit
// toute l'interface, y compris apres avoir quitte le PIE. On ne l'applique
// donc qu'en jeu. La luminosite ne se teste pas en PIE, mais en
// Standalone -- qui est un processus separe.
if (GEngine && !GIsEditor)
{
using namespace SurvivalSettingsRange;
const float Normalized = FMath::Clamp(Brightness, BrightnessMin, BrightnessMax) / 100.f;
GEngine->DisplayGamma = BaseDisplayGamma * Normalized;
}
OnSurvivalSettingsApplied.Broadcast();
}
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()
{
Super::ApplyNonResolutionSettings();
// ApplySettings() passe forcement par ici : brancher la diffusion a ce
// niveau garantit que nos reglages suivent, quel que soit le chemin.
ApplySurvivalSettings();
ApplyAudioSettings();
}
float USurvivalUserSettings::GetLookMultiplierX() const
{
using namespace SurvivalSettingsRange;
const float Normalized = FMath::GetRangePct(SensitivityMin, SensitivityMax, LookSensitivity);
return FMath::Lerp(LookMultiplierAtMin, LookMultiplierAtMax, Normalized);
}
float USurvivalUserSettings::GetLookMultiplierY() const
{
using namespace SurvivalSettingsRange;
// Sans axe separe, le vertical suit l'horizontal : c'est ce que veulent
// 95 % des joueurs, et deux curseurs desynchronises par inadvertance
// donnent une visee bancale sans qu'on comprenne pourquoi.
if (!bSeparateVerticalSensitivity)
{
return GetLookMultiplierX();
}
const float Normalized = FMath::GetRangePct(SensitivityMin, SensitivityMax, LookSensitivityVertical);
return FMath::Lerp(LookMultiplierAtMin, LookMultiplierAtMax, Normalized);
}
float USurvivalUserSettings::GetCameraShakeMultiplier() const
{
return FMath::Clamp(CameraShakeScale / 100.f, 0.f, 1.f);
}
void USurvivalUserSettings::SetLookSensitivity(float Value)
{
using namespace SurvivalSettingsRange;
LookSensitivity = SnapToStep(Value, SensitivityMin, SensitivityMax, SensitivityStep);
}
void USurvivalUserSettings::SetSeparateVerticalSensitivity(bool bValue)
{
// En repassant sur "non separe" on recale le vertical sur l'horizontal :
// sinon la valeur oubliee ressortirait telle quelle a la reactivation.
if (!bValue)
{
LookSensitivityVertical = LookSensitivity;
}
bSeparateVerticalSensitivity = bValue;
}
void USurvivalUserSettings::SetLookSensitivityVertical(float Value)
{
using namespace SurvivalSettingsRange;
LookSensitivityVertical = SnapToStep(Value, SensitivityMin, SensitivityMax, SensitivityStep);
}
void USurvivalUserSettings::SetInvertLookY(bool bValue)
{
bInvertLookY = bValue;
}
void USurvivalUserSettings::SetFieldOfView(float Value)
{
using namespace SurvivalSettingsRange;
FieldOfView = SnapToStep(Value, FieldOfViewMin, FieldOfViewMax, FieldOfViewStep);
}
void USurvivalUserSettings::SetCameraShakeScale(float Value)
{
using namespace SurvivalSettingsRange;
CameraShakeScale = SnapToStep(Value, CameraShakeMin, CameraShakeMax, CameraShakeStep);
}
void USurvivalUserSettings::SetSprintToggle(bool bValue)
{
bSprintToggle = bValue;
}
void USurvivalUserSettings::SetCrouchToggle(bool bValue)
{
bCrouchToggle = bValue;
}
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;
}
void USurvivalUserSettings::SetCharacterAppearance(const FCharacterAppearance& InAppearance)
{
CharacterAppearance = InAppearance;
}