// 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()->DefaultBaseSoundMix; return MixPath.IsValid() ? Cast(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(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; }