diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 40146f0..4575838 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -115,6 +115,7 @@ qu'une compilation propre l'est vraiment. | Système | Fichiers | |---|---| | 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` | | Interaction (trace + interface) | `Interactable.h`, `InteractionComponent`, `InteractionPromptWidget` | | Objets | `ItemDataAsset`, `PickupItem` | @@ -127,6 +128,8 @@ qu'une compilation propre l'est vraiment. | Écran à onglets (inventaire / craft) | `InventoryScreenWidget` | | Menu de pause | `PauseMenuWidget` + la partie pause de `FpsPlayerController` | | Réglages | `SurvivalUserSettings`, `SettingsMenuWidget`, `SettingsTabWidget`, `SettingsGameplayWidget`, `SettingRowWidget` | +| Musique | `MusicDirector`, `MusicSubsystem`, `MusicPlaylistDataAsset` | +| Audio (volumes) | `SurvivalAudioSettings`, `SettingsAudioWidget` + `USurvivalUserSettings::ApplyAudioSettings` | | Fondus, boîtes de dialogue | `ScreenFadeComponent`, `ConfirmDialogWidget` | | Coop Steam (sessions) | `SessionSubsystem`, `SurvivalGameMode`, `SurvivalGameState`, `SurvivalPlayerState` | | 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 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 - **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. -> **Systèmes présents dans `Source/` mais pas encore décrits ici** : `MusicDirector`, -> `MusicSubsystem`, `MusicPlaylistDataAsset`, `SettingsDisplayWidget`, `SettingsGraphicsWidget`, +> **Systèmes présents dans `Source/` mais pas encore décrits ici** : +> `SettingsDisplayWidget`, `SettingsGraphicsWidget`, > `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 > décrire de mémoire un code qu'on n'a pas lu. diff --git a/Config/DefaultEngine.ini b/Config/DefaultEngine.ini index 14aed2f..752d00e 100644 --- a/Config/DefaultEngine.ini +++ b/Config/DefaultEngine.ini @@ -146,3 +146,7 @@ ConnectionType=USBOnly bUseManualIPAddress=False ManualIPAddress= +[/Script/Engine.AudioSettings] +DefaultSoundClassName=/Game/Game/Sounds/Mix/SC_SFX.SC_SFX +DefaultBaseSoundMix=/Game/Game/Sounds/Mix/SM_Settings.SM_Settings + diff --git a/Config/DefaultGame.ini b/Config/DefaultGame.ini index 3e484e1..d5a0d6e 100644 --- a/Config/DefaultGame.ini +++ b/Config/DefaultGame.ini @@ -52,3 +52,9 @@ bMoviesAreSkippable=True +StartupMovies=UnrealLogo +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 + diff --git a/Content/Game/Animation/Anim_BP/ABP_FpsArms.uasset b/Content/Game/Animation/Anim_BP/ABP_FpsArms.uasset new file mode 100644 index 0000000..dc886f7 --- /dev/null +++ b/Content/Game/Animation/Anim_BP/ABP_FpsArms.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cdfc0bde30b650799675ed20e81b16b0aab6f515589888322124f18d9e020546 +size 103357 diff --git a/Content/Game/Animation/Fps/Armor_Fur_mat.uasset b/Content/Game/Animation/Fps/Armor_Fur_mat.uasset deleted file mode 100644 index 3904607..0000000 --- a/Content/Game/Animation/Fps/Armor_Fur_mat.uasset +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6ab0269e99d8e5e7179f7fed27abc2a0b85423dd5c4b09b87f116f56ec77e3c7 -size 61450 diff --git a/Content/Game/Animation/Fps/Armor_Upper_mat.uasset b/Content/Game/Animation/Fps/Armor_Upper_mat.uasset deleted file mode 100644 index 5ae49e7..0000000 --- a/Content/Game/Animation/Fps/Armor_Upper_mat.uasset +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c76563f24e8b50a5b1123d596df351eee1f6a28da3e948884dcf02c9c46d648e -size 61484 diff --git a/Content/Game/Animation/Fps/BasePose.uasset b/Content/Game/Animation/Fps/BasePose.uasset new file mode 100644 index 0000000..db0cb9f --- /dev/null +++ b/Content/Game/Animation/Fps/BasePose.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2e9e12ae4ffe638a1f3cab9e512c5034d0f2c3c9f1554b409d921cfcc2d5a587 +size 524354 diff --git a/Content/Game/Animation/Fps/BasePose_Skeleton.uasset b/Content/Game/Animation/Fps/BasePose_Skeleton.uasset new file mode 100644 index 0000000..0462b3c --- /dev/null +++ b/Content/Game/Animation/Fps/BasePose_Skeleton.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6e7f2f50428bf2c798769cc5f22c923079a6b3c6c437a522f5bedd3894f6ff3d +size 30620 diff --git a/Content/Game/Animation/Fps/BasePose__2_.uasset b/Content/Game/Animation/Fps/BasePose__2_.uasset deleted file mode 100644 index 348fe5c..0000000 --- a/Content/Game/Animation/Fps/BasePose__2_.uasset +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a25d6616f50e8ed026d08f9f487d874f1371aeeccd5ad149eb1a46a8127f4ae0 -size 4888213 diff --git a/Content/Game/Animation/Fps/BasePose__2__Anim.uasset b/Content/Game/Animation/Fps/BasePose__2__Anim.uasset deleted file mode 100644 index 323c41a..0000000 --- a/Content/Game/Animation/Fps/BasePose__2__Anim.uasset +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4c5fcd7af8da287e23fab18651f49176a095414a23271a2a9e950aa7cc21f45e -size 411834 diff --git a/Content/Game/Animation/Fps/BasePose__2__PhysicsAsset.uasset b/Content/Game/Animation/Fps/BasePose__2__PhysicsAsset.uasset deleted file mode 100644 index 2f6a86f..0000000 --- a/Content/Game/Animation/Fps/BasePose__2__PhysicsAsset.uasset +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c672dce24656db511bb153521b44c6dcf918b5021261459e7976543e16bbaae1 -size 11028 diff --git a/Content/Game/Animation/Fps/BasePose__2__Skeleton.uasset b/Content/Game/Animation/Fps/BasePose__2__Skeleton.uasset deleted file mode 100644 index c080a80..0000000 --- a/Content/Game/Animation/Fps/BasePose__2__Skeleton.uasset +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3a7de911125531c5a58fc4f87bd9a359ae9dc8f2d21ca11ac978ec559051cc43 -size 29645 diff --git a/Content/Game/Animation/Fps/GrabItem.uasset b/Content/Game/Animation/Fps/GrabItem.uasset index c9090cc..a7b52ab 100644 --- a/Content/Game/Animation/Fps/GrabItem.uasset +++ b/Content/Game/Animation/Fps/GrabItem.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dfc7bb305b3f317fdfa1b42fcbe60ee546f00e7a9ed7ab929116ee28e2ae8f21 -size 418198 +oid sha256:ae5188b2fdde0a8a37fe595c8cdc56bf29141932ab148e5b9f69b24f925f19af +size 417911 diff --git a/Content/Game/Animation/Fps/MaleBody_M.uasset b/Content/Game/Animation/Fps/MaleBody_M.uasset index f06347f..0606456 100644 --- a/Content/Game/Animation/Fps/MaleBody_M.uasset +++ b/Content/Game/Animation/Fps/MaleBody_M.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2645ae3ad17dc491b6b0334f2641fabd48c2ae5366071a9c9f3b45206e30cd34 -size 62565 +oid sha256:c55eede3767346d4036d65b602a23f48f25f301e2512a083580a8dc3eb6cf304 +size 62560 diff --git a/Content/Game/Animation/Fps/Run_NoItem.uasset b/Content/Game/Animation/Fps/Run_NoItem.uasset index 3b83965..958fa73 100644 --- a/Content/Game/Animation/Fps/Run_NoItem.uasset +++ b/Content/Game/Animation/Fps/Run_NoItem.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b058c7094cc99b7682baa4924d12dba67811baffa6b7f67d53b3f18b33e117fa -size 441628 +oid sha256:fffd98c6a6f6a70bf962db547775d5db9efca7bad9d5d02a32caf5d60fec9d4d +size 441589 diff --git a/Content/Game/Animation/Fps/lambert3.uasset b/Content/Game/Animation/Fps/lambert3.uasset deleted file mode 100644 index 7cf9567..0000000 --- a/Content/Game/Animation/Fps/lambert3.uasset +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:97d7b3a841f88ab3ccee7a4668887fca551a9649866c29d5141dc86633c679c9 -size 62153 diff --git a/Content/Game/Animation/Fps/lambert4.uasset b/Content/Game/Animation/Fps/lambert4.uasset deleted file mode 100644 index 00d6dd7..0000000 --- a/Content/Game/Animation/Fps/lambert4.uasset +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d2d9ab871b1dff347d636b6d1cf69511fcb9c6ba5e464abb07bed93a252da3d8 -size 62153 diff --git a/Content/Game/Audio/Music/echonn-the-wandererx27s-song-220954.uasset b/Content/Game/Audio/Music/echonn-the-wandererx27s-song-220954.uasset deleted file mode 100644 index c562fd5..0000000 --- a/Content/Game/Audio/Music/echonn-the-wandererx27s-song-220954.uasset +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:af4b0724c6fe90e5eff787cde40a2e4a74afb97d4da5923230648d8f1209f764 -size 21411384 diff --git a/Content/Game/Audio/Music/grand_project-mystical-forest-384201.uasset b/Content/Game/Audio/Music/grand_project-mystical-forest-384201.uasset deleted file mode 100644 index 4fda4f8..0000000 --- a/Content/Game/Audio/Music/grand_project-mystical-forest-384201.uasset +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6cf2192b426af0bc4551551e2631b0f5923164d14f4c33dbee2851bdc3a462df -size 10888716 diff --git a/Content/Game/Audio/Music/grand_project-wonders-of-the-earth-550792.uasset b/Content/Game/Audio/Music/grand_project-wonders-of-the-earth-550792.uasset deleted file mode 100644 index f130348..0000000 --- a/Content/Game/Audio/Music/grand_project-wonders-of-the-earth-550792.uasset +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8716134a0966a3618a8deb78b397b47845aa58375760ea2339d7d5699d876473 -size 17312587 diff --git a/Content/Game/Audio/Music/viramiller-nature-investigation-255161.uasset b/Content/Game/Audio/Music/viramiller-nature-investigation-255161.uasset deleted file mode 100644 index edd7355..0000000 --- a/Content/Game/Audio/Music/viramiller-nature-investigation-255161.uasset +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4ac1b1969f61639a0095a02168366ab610f68603bfb2a3d328308867dc4417a2 -size 6743336 diff --git a/Content/Game/Audio/Playlists/DA_Playlist_Ambient.uasset b/Content/Game/Audio/Playlists/DA_Playlist_Ambient.uasset deleted file mode 100644 index 5c7c820..0000000 --- a/Content/Game/Audio/Playlists/DA_Playlist_Ambient.uasset +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0eacbf919ba68e2baa8a1d7614177ad9f4eae9eaed5da06280ed5cce8c4d4c53 -size 2797 diff --git a/Content/Game/Input/IMC_Default.uasset b/Content/Game/Input/IMC_Default.uasset index cd5905e..44c9a67 100644 --- a/Content/Game/Input/IMC_Default.uasset +++ b/Content/Game/Input/IMC_Default.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5059aebc29d2c39f95ba557afe88e0d8cc89277aa7d0734089f3565c778ce422 -size 16786 +oid sha256:f2a1829fb9833838cbf30aa8a493c4cdcdaaab11b34fabeacc7cefb133bad00d +size 18607 diff --git a/Content/Game/Maps/Dev/Dev_Scene.umap b/Content/Game/Maps/Dev/Dev_Scene.umap index 8f1aa58..8bf7d6c 100644 --- a/Content/Game/Maps/Dev/Dev_Scene.umap +++ b/Content/Game/Maps/Dev/Dev_Scene.umap @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:99720fda0ee1f8ca242950dd526aeb26f391b04a9d500bf6aef0846546ef7ee8 -size 125999 +oid sha256:893bf5fea0da89dba79fa654eb3f7198fd8f0ec4e0214d31274acd3a26c81781 +size 121698 diff --git a/Content/Game/Maps/MenuScene.umap b/Content/Game/Maps/MenuScene.umap index 83825a2..a39c130 100644 --- a/Content/Game/Maps/MenuScene.umap +++ b/Content/Game/Maps/MenuScene.umap @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8748372e1c97adbce152e816b693281ff9c4980b6d5c02abf11a657f63aec9cd -size 15120890 +oid sha256:1168f1ce5680507bb24f7092a0f381c03b0f2386188a4828bba46d0343e89baa +size 15120887 diff --git a/Content/Game/Player/BP_FpsPlayer.uasset b/Content/Game/Player/BP_FpsPlayer.uasset index 6c9bd21..71b88f0 100644 --- a/Content/Game/Player/BP_FpsPlayer.uasset +++ b/Content/Game/Player/BP_FpsPlayer.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d71bb277d0f226b071bbd5d2bdb1c3802225d9b233ecb2250c438ec6a97bf8c7 -size 29950 +oid sha256:12293bc8a214bf2f914ef2add11475bbe8ce39ae7b50f18474fb588cfa405e24 +size 36945 diff --git a/Content/Game/Sounds/Mix/SC_Master.uasset b/Content/Game/Sounds/Mix/SC_Master.uasset new file mode 100644 index 0000000..38c37fd --- /dev/null +++ b/Content/Game/Sounds/Mix/SC_Master.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:757ecab8f26c1c0f637866810fd9e62311815c37c0d620db0bd57edb25d27425 +size 1561 diff --git a/Content/Game/Sounds/Mix/SC_Music.uasset b/Content/Game/Sounds/Mix/SC_Music.uasset new file mode 100644 index 0000000..4b3dcd8 --- /dev/null +++ b/Content/Game/Sounds/Mix/SC_Music.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:80ca65eb4ab29400ceeb47c0ba6f182ee00a185e4c9a739a6347100d37d5ada6 +size 1249 diff --git a/Content/Game/Sounds/Mix/SC_SFX.uasset b/Content/Game/Sounds/Mix/SC_SFX.uasset new file mode 100644 index 0000000..db43dd6 --- /dev/null +++ b/Content/Game/Sounds/Mix/SC_SFX.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9c1e3a63073e8278b32d1f41cc584442c81d92493f883ffad5a3b3af5b16048f +size 1239 diff --git a/Content/Game/Sounds/Mix/SC_UI.uasset b/Content/Game/Sounds/Mix/SC_UI.uasset new file mode 100644 index 0000000..eea641b --- /dev/null +++ b/Content/Game/Sounds/Mix/SC_UI.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:16c2eda782b90f392cf0f7235e26078c0142d3b15ad1e18e4e066a6d445ae675 +size 1234 diff --git a/Content/Game/Sounds/Mix/SM_Settings.uasset b/Content/Game/Sounds/Mix/SM_Settings.uasset new file mode 100644 index 0000000..584c547 --- /dev/null +++ b/Content/Game/Sounds/Mix/SM_Settings.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8f5872020fbbe7127f607b2a81e82de6dc57c68bbc7e16f7234acd1df436b343 +size 1071 diff --git a/Content/Game/Sounds/Music/echonn-the-wandererx27s-song-220954.uasset b/Content/Game/Sounds/Music/echonn-the-wandererx27s-song-220954.uasset new file mode 100644 index 0000000..10148c3 --- /dev/null +++ b/Content/Game/Sounds/Music/echonn-the-wandererx27s-song-220954.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:368f4a5d6a5c5e97ffbe484bbe84995716e0c553ef6512bddbf883833dd8c7a9 +size 21411657 diff --git a/Content/Game/Sounds/Music/grand_project-mystical-forest-384201.uasset b/Content/Game/Sounds/Music/grand_project-mystical-forest-384201.uasset new file mode 100644 index 0000000..ce532c3 --- /dev/null +++ b/Content/Game/Sounds/Music/grand_project-mystical-forest-384201.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:53f8f56c116611268882075c4bd776ba136a4b320d4580fbcce0389e73a738e4 +size 10888989 diff --git a/Content/Game/Sounds/Music/grand_project-wonders-of-the-earth-550792.uasset b/Content/Game/Sounds/Music/grand_project-wonders-of-the-earth-550792.uasset new file mode 100644 index 0000000..34e0d7f --- /dev/null +++ b/Content/Game/Sounds/Music/grand_project-wonders-of-the-earth-550792.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:78509792b484bfabf1c3cc5bbff30495229b1cadd8cf78195e9b2fa6a4af6318 +size 17312860 diff --git a/Content/Game/Sounds/Music/viramiller-nature-investigation-255161.uasset b/Content/Game/Sounds/Music/viramiller-nature-investigation-255161.uasset new file mode 100644 index 0000000..400080f --- /dev/null +++ b/Content/Game/Sounds/Music/viramiller-nature-investigation-255161.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c24815111eb6a2c57751b1d8936de5450ec4e854f1465c3102f70bf5f66d8e93 +size 6743609 diff --git a/Content/Game/Sounds/Playlists/DA_Playlist_Ambient.uasset b/Content/Game/Sounds/Playlists/DA_Playlist_Ambient.uasset new file mode 100644 index 0000000..5395d98 --- /dev/null +++ b/Content/Game/Sounds/Playlists/DA_Playlist_Ambient.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d7e2c14940e605d78e2ddc6c67024a0b8330cfbee3491c7f0ae07c5379388a83 +size 2804 diff --git a/Content/Game/Sounds/UI/Hover/47313572-ui-sounds-pack-3-10-359727.uasset b/Content/Game/Sounds/UI/Hover/47313572-ui-sounds-pack-3-10-359727.uasset index 6a383e4..14901a0 100644 --- a/Content/Game/Sounds/UI/Hover/47313572-ui-sounds-pack-3-10-359727.uasset +++ b/Content/Game/Sounds/UI/Hover/47313572-ui-sounds-pack-3-10-359727.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:18ff8d4f869a985f4d8ecfc016b7f48898b743475aafb5b9618a514978a7e0af -size 14093 +oid sha256:e98120f1f97e7182942cf2916df92f82df6411e02054e7f0ce102fbc3e8f823d +size 14352 diff --git a/Content/Game/Sounds/UI/Hover/47313572-ui-sounds-pack-3-11-359714.uasset b/Content/Game/Sounds/UI/Hover/47313572-ui-sounds-pack-3-11-359714.uasset index 710e751..7ecc013 100644 --- a/Content/Game/Sounds/UI/Hover/47313572-ui-sounds-pack-3-11-359714.uasset +++ b/Content/Game/Sounds/UI/Hover/47313572-ui-sounds-pack-3-11-359714.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b7bdda65d6838e07e757c34ffae89819dee0bc0e347b58fe7aa9fdf9a401c281 -size 10732 +oid sha256:fde977dad2d125446d2bb2d7bc1bb7d3101dbde2f1ffa00141b5cf2a06acd508 +size 10991 diff --git a/Content/Game/Sounds/UI/Hover/47313572-ui-sounds-pack-3-14-359717.uasset b/Content/Game/Sounds/UI/Hover/47313572-ui-sounds-pack-3-14-359717.uasset index 1e84df7..14f5c14 100644 --- a/Content/Game/Sounds/UI/Hover/47313572-ui-sounds-pack-3-14-359717.uasset +++ b/Content/Game/Sounds/UI/Hover/47313572-ui-sounds-pack-3-14-359717.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8331df2b3874695587e69461fe9c64dffb11f5d6643d26cddc99f47da075853b -size 16775 +oid sha256:847c452a5f81609496d1683f740df875728ec86d2d08b3e5d3cfe54d0ad3f9c0 +size 17034 diff --git a/Content/Game/UI/Menu/WBP_MainMenu.uasset b/Content/Game/UI/Menu/WBP_MainMenu.uasset index 2b67e21..6d376f3 100644 --- a/Content/Game/UI/Menu/WBP_MainMenu.uasset +++ b/Content/Game/UI/Menu/WBP_MainMenu.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:92cc18212d7697008d6d7b8f52e164c6275c0c4abf0e360e32ba138c863f51e1 -size 211194 +oid sha256:c6b707fdce9a47bba91695b294c67ff55a3cacde4ec48ac7012b5b7f32de8314 +size 211435 diff --git a/Content/Game/UI/Setting/WBP_Settings.uasset b/Content/Game/UI/Setting/WBP_Settings.uasset index 03b0ecb..ffd3404 100644 --- a/Content/Game/UI/Setting/WBP_Settings.uasset +++ b/Content/Game/UI/Setting/WBP_Settings.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d99dae1ac8a99ce499a49d968cf05d5a641cdedb338cd25c19c90ee6c573544d -size 53466 +oid sha256:bd240436529609878d93b3bf479e8e6935577b33f4fa168ff48e963f7c8e0f05 +size 56876 diff --git a/Content/Game/UI/Setting/WBP_SettingsAudio.uasset b/Content/Game/UI/Setting/WBP_SettingsAudio.uasset new file mode 100644 index 0000000..01fe678 --- /dev/null +++ b/Content/Game/UI/Setting/WBP_SettingsAudio.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3d961e5f714a36a04e75b8b18ac3ef9cd23cd4486c9ae1158dbfba605a3f5951 +size 29694 diff --git a/Source/Survival_projet/Private/FpsArmsAnimInstance.cpp b/Source/Survival_projet/Private/FpsArmsAnimInstance.cpp new file mode 100644 index 0000000..6ece8b9 --- /dev/null +++ b/Source/Survival_projet/Private/FpsArmsAnimInstance.cpp @@ -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(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(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); +} diff --git a/Source/Survival_projet/Private/FpsPlayer.cpp b/Source/Survival_projet/Private/FpsPlayer.cpp index b964cf1..35603db 100644 --- a/Source/Survival_projet/Private/FpsPlayer.cpp +++ b/Source/Survival_projet/Private/FpsPlayer.cpp @@ -3,6 +3,8 @@ #include "FpsPlayer.h" +#include "Animation/AnimInstance.h" +#include "Animation/AnimMontage.h" #include "Camera/CameraComponent.h" #include "Camera/CameraTypes.h" #include "Components/CapsuleComponent.h" @@ -14,11 +16,13 @@ #include "GameFramework/PlayerController.h" #include "InputActionValue.h" #include "CraftingComponent.h" +#include "HeadBobComponent.h" #include "InteractionComponent.h" #include "InventoryComponent.h" #include "ItemDataAsset.h" #include "Net/UnrealNetwork.h" #include "PickupItem.h" +#include "SurvivalGameInstance.h" #include "SurvivalStatsComponent.h" #include "SurvivalUserSettings.h" @@ -74,6 +78,31 @@ AFpsPlayer::AFpsPlayer() // ecraserait la rotation chaque frame et tous les effets de tilt seraient invisibles. FirstPersonCamera->bUsePawnControlRotation = false; + // Bras vue premiere personne, accroches a la camera pour suivre le regard. + FirstPersonArms = CreateDefaultSubobject(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(TEXT("HeadBob")); + InteractionComponent = CreateDefaultSubobject(TEXT("InteractionComponent")); InventoryComponent = CreateDefaultSubobject(TEXT("InventoryComponent")); CraftingComponent = CreateDefaultSubobject(TEXT("CraftingComponent")); @@ -131,10 +160,18 @@ void AFpsPlayer::BeginPlay() { if (DefaultMappingContext) { - // On ACTIVE le contexte ici, mais on ne le DECLARE pas comme - // remappable : c'est USurvivalGameInstance qui s'en charge, parce - // que le menu principal n'a pas de pawn et doit quand meme - // afficher les touches. + // La DECLARATION des contextes remappables appartient au + // GameInstance -- le menu principal n'a pas de pawn et doit + // pourtant afficher les touches. On la demande quand meme ici, et + // 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()) + { + GameInstance->RegisterMappableContexts(); + } + Subsystem->AddMappingContext(DefaultMappingContext, 0); } else @@ -176,6 +213,10 @@ void AFpsPlayer::ApplyUserSettings() bSprintToggleMode = Settings->GetSprintToggle(); 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 // personnage bloque en sprint : on repart d'un etat propre. if (!bSprintToggleMode && bIsSprinting) @@ -228,7 +269,15 @@ void AFpsPlayer::UpdateCameraEffects(float DeltaTime) 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 // 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 -- // a condition de lire les deux au meme instant, d'ou l'appel depuis CalcCamera. 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 @@ -274,6 +334,9 @@ void AFpsPlayer::ApplyCameraTransform() // regarde vers le bas. C'est ce qui vend l'impact a l'atterrissage. 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()) { FirstPersonCamera->SetWorldRotation(OwningController->GetControlRotation() + ViewOffset); @@ -812,9 +875,35 @@ void AFpsPlayer::StopCrouch() 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(InteractionComponent->GetFocusedActor())) + { + PlayArmsMontage(GrabMontage); + } + 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) { Super::OnStartCrouch(HalfHeightAdjust, ScaledHalfHeightAdjust); diff --git a/Source/Survival_projet/Private/HeadBobComponent.cpp b/Source/Survival_projet/Private/HeadBobComponent.cpp new file mode 100644 index 0000000..fd84012 --- /dev/null +++ b/Source/Survival_projet/Private/HeadBobComponent.cpp @@ -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(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); +} diff --git a/Source/Survival_projet/Private/KeyboardLayout.cpp b/Source/Survival_projet/Private/KeyboardLayout.cpp new file mode 100644 index 0000000..e8c20db --- /dev/null +++ b/Source/Survival_projet/Private/KeyboardLayout.cpp @@ -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 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& GetReferenceScanCodes() + { + static const TMap ScanCodes = [] + { + TMap 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 + } +} diff --git a/Source/Survival_projet/Private/MusicSubsystem.cpp b/Source/Survival_projet/Private/MusicSubsystem.cpp index 8889288..dc65fbd 100644 --- a/Source/Survival_projet/Private/MusicSubsystem.cpp +++ b/Source/Survival_projet/Private/MusicSubsystem.cpp @@ -9,6 +9,8 @@ #include "Kismet/GameplayStatics.h" #include "MusicPlaylistDataAsset.h" #include "Sound/SoundBase.h" +#include "Sound/SoundClass.h" +#include "SurvivalAudioSettings.h" #include "UObject/UObjectGlobals.h" UMusicSubsystem* UMusicSubsystem::Get(const UObject* WorldContextObject) @@ -298,6 +300,20 @@ void UMusicSubsystem::PlayTrackAtCursor(float FadeInDuration) 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) { MusicComponent = CreateMusicComponent(Track); @@ -353,6 +369,14 @@ UAudioComponent* UMusicSubsystem::CreateMusicComponent(USoundBase* Sound) 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->OnAudioFinished.AddDynamic(this, &UMusicSubsystem::HandleTrackFinished); return Component; diff --git a/Source/Survival_projet/Private/SettingsAudioWidget.cpp b/Source/Survival_projet/Private/SettingsAudioWidget.cpp new file mode 100644 index 0000000..f69d5eb --- /dev/null +++ b/Source/Survival_projet/Private/SettingsAudioWidget.cpp @@ -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 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 diff --git a/Source/Survival_projet/Private/SettingsControlsWidget.cpp b/Source/Survival_projet/Private/SettingsControlsWidget.cpp index 2daecdf..9a05854 100644 --- a/Source/Survival_projet/Private/SettingsControlsWidget.cpp +++ b/Source/Survival_projet/Private/SettingsControlsWidget.cpp @@ -186,6 +186,16 @@ void USettingsControlsWidget::ResetToDefaults() } 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()) + { + GameInstance->ApplyKeyboardLayoutToDefaultKeys(); + } + RefreshFromSettings(); } diff --git a/Source/Survival_projet/Private/SettingsMenuWidget.cpp b/Source/Survival_projet/Private/SettingsMenuWidget.cpp index d4becdd..3119cb4 100644 --- a/Source/Survival_projet/Private/SettingsMenuWidget.cpp +++ b/Source/Survival_projet/Private/SettingsMenuWidget.cpp @@ -18,8 +18,8 @@ namespace { - // Ordre des pages dans le WidgetSwitcher. Les trois dernieres n'existent pas - // encore : leur bouton se grise tout seul tant que la page est absente. + // Ordre des pages dans le WidgetSwitcher. Un bouton dont la page manque se + // grise tout seul, ce qui reste vrai si un onglet vient a etre retire. constexpr int32 TabIndexGameplay = 0; constexpr int32 TabIndexDisplay = 1; constexpr int32 TabIndexGraphics = 2; diff --git a/Source/Survival_projet/Private/SurvivalAudioSettings.cpp b/Source/Survival_projet/Private/SurvivalAudioSettings.cpp new file mode 100644 index 0000000..7e6c50e --- /dev/null +++ b/Source/Survival_projet/Private/SurvivalAudioSettings.cpp @@ -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(); +} + +USoundClass* USurvivalAudioSettings::Resolve(const TSoftObjectPtr& Asset, TStrongObjectPtr& 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); +} diff --git a/Source/Survival_projet/Private/SurvivalGameInstance.cpp b/Source/Survival_projet/Private/SurvivalGameInstance.cpp index 8a68f8f..04cbf06 100644 --- a/Source/Survival_projet/Private/SurvivalGameInstance.cpp +++ b/Source/Survival_projet/Private/SurvivalGameInstance.cpp @@ -10,6 +10,7 @@ #include "Engine/LocalPlayer.h" #include "GameMapsSettings.h" #include "InputMappingContext.h" +#include "KeyboardLayout.h" #include "Kismet/GameplayStatics.h" #include "SessionSubsystem.h" #include "SurvivalUserSettings.h" @@ -233,13 +234,131 @@ void USurvivalGameInstance::RegisterMappableContexts() return; } + bool bRegisteredAnyContext = false; + for (const TObjectPtr& Context : MappableContexts) { if (Context) { + bRegisteredAnyContext = true; // Idempotent : le moteur ignore un contexte deja connu, on peut donc // rappeler cette fonction sans precaution. 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(); + 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 PendingRemaps; + + for (const TPair& 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(); } diff --git a/Source/Survival_projet/Private/SurvivalUserSettings.cpp b/Source/Survival_projet/Private/SurvivalUserSettings.cpp index c71f926..8f35a6c 100644 --- a/Source/Survival_projet/Private/SurvivalUserSettings.cpp +++ b/Source/Survival_projet/Private/SurvivalUserSettings.cpp @@ -3,7 +3,14 @@ #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 { @@ -13,6 +20,88 @@ namespace 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() @@ -22,6 +111,7 @@ USurvivalUserSettings::USurvivalUserSettings() // fichier s'il existe. SetSurvivalGameplayToDefaults(); SetSurvivalDisplayToDefaults(); + SetSurvivalAudioToDefaults(); } USurvivalUserSettings* USurvivalUserSettings::Get() @@ -46,6 +136,22 @@ 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 : @@ -54,6 +160,7 @@ void USurvivalUserSettings::SetToDefaults() SetSurvivalGameplayToDefaults(); SetSurvivalDisplayToDefaults(); + SetSurvivalAudioToDefaults(); } void USurvivalUserSettings::SetBrightness(float Value) @@ -85,6 +192,40 @@ void USurvivalUserSettings::ApplySurvivalSettings() 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(); @@ -92,6 +233,7 @@ void USurvivalUserSettings::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 @@ -173,3 +315,32 @@ 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; +} diff --git a/Source/Survival_projet/Public/FpsArmsAnimInstance.h b/Source/Survival_projet/Public/FpsArmsAnimInstance.h new file mode 100644 index 0000000..6606282 --- /dev/null +++ b/Source/Survival_projet/Public/FpsArmsAnimInstance.h @@ -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 OwningPlayer; +}; diff --git a/Source/Survival_projet/Public/FpsPlayer.h b/Source/Survival_projet/Public/FpsPlayer.h index c509259..b428a82 100644 --- a/Source/Survival_projet/Public/FpsPlayer.h +++ b/Source/Survival_projet/Public/FpsPlayer.h @@ -7,8 +7,10 @@ #include "FpsPlayer.generated.h" class APickupItem; +class UAnimMontage; class UCameraComponent; class UCraftingComponent; +class UHeadBobComponent; class UInputAction; class UInputMappingContext; class UInteractionComponent; @@ -88,6 +90,26 @@ public: UFUNCTION(BlueprintPure, Category = "Camera") 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") UInteractionComponent* GetInteractionComponent() const { return InteractionComponent; } @@ -198,6 +220,40 @@ protected: UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera") TObjectPtr 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 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 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 HeadBob; + /** Détecte et déclenche les interactions avec le monde. */ UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Interaction") TObjectPtr InteractionComponent; diff --git a/Source/Survival_projet/Public/HeadBobComponent.h b/Source/Survival_projet/Public/HeadBobComponent.h new file mode 100644 index 0000000..5148f35 --- /dev/null +++ b/Source/Survival_projet/Public/HeadBobComponent.h @@ -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 OwnerCharacter; + + TObjectPtr OwnerMovement; +}; diff --git a/Source/Survival_projet/Public/KeyboardLayout.h b/Source/Survival_projet/Public/KeyboardLayout.h new file mode 100644 index 0000000..60b82bb --- /dev/null +++ b/Source/Survival_projet/Public/KeyboardLayout.h @@ -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(); +} diff --git a/Source/Survival_projet/Public/MusicSubsystem.h b/Source/Survival_projet/Public/MusicSubsystem.h index 161a3a6..5e19fe1 100644 --- a/Source/Survival_projet/Public/MusicSubsystem.h +++ b/Source/Survival_projet/Public/MusicSubsystem.h @@ -114,7 +114,15 @@ public: UFUNCTION(BlueprintPure, Category = "Musique") 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") void SetMusicVolume(float NewVolume); diff --git a/Source/Survival_projet/Public/SettingsAudioWidget.h b/Source/Survival_projet/Public/SettingsAudioWidget.h new file mode 100644 index 0000000..d239862 --- /dev/null +++ b/Source/Survival_projet/Public/SettingsAudioWidget.h @@ -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 MasterVolumeRow; + + UPROPERTY(meta = (BindWidget)) + TObjectPtr MusicVolumeRow; + + UPROPERTY(meta = (BindWidget)) + TObjectPtr EffectsVolumeRow; + + UPROPERTY(meta = (BindWidget)) + TObjectPtr UiVolumeRow; + + UPROPERTY(meta = (BindWidget)) + TObjectPtr 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); +}; diff --git a/Source/Survival_projet/Public/SettingsMenuWidget.h b/Source/Survival_projet/Public/SettingsMenuWidget.h index b5dea27..7458640 100644 --- a/Source/Survival_projet/Public/SettingsMenuWidget.h +++ b/Source/Survival_projet/Public/SettingsMenuWidget.h @@ -124,8 +124,7 @@ protected: TObjectPtr ResetButton; /** - * Les trois onglets qui existent sont OBLIGATOIRES : un WBP_Settings sans eux - * ne compile pas. + * Les cinq onglets sont OBLIGATOIRES : un WBP_Settings sans eux ne compile pas. * * BindWidgetOptional echoue en silence -- un nom mal orthographie donnait un * bouton parfaitement inerte, sans le moindre message. Pour un element dont @@ -143,12 +142,7 @@ protected: UPROPERTY(meta = (BindWidget)) TObjectPtr ControlsTabButton; - /** - * Celui-la reste optionnel : sa page n'existe pas encore. Il passera en - * BindWidget le jour ou l'onglet Audio sera ecrit -- en attendant, il se - * grise tout seul si tu le poses. - */ - UPROPERTY(meta = (BindWidgetOptional)) + UPROPERTY(meta = (BindWidget)) TObjectPtr AudioTabButton; /** diff --git a/Source/Survival_projet/Public/SurvivalAudioSettings.h b/Source/Survival_projet/Public/SurvivalAudioSettings.h new file mode 100644 index 0000000..ae67bd9 --- /dev/null +++ b/Source/Survival_projet/Public/SurvivalAudioSettings.h @@ -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 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 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 EffectsSoundClass; + + /** A poser a la main sur les sons d'interface (survol, validation). */ + UPROPERTY(config, EditAnywhere, Category = "Sound Classes") + TSoftObjectPtr 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& Asset, TStrongObjectPtr& 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 CachedMasterSoundClass; + + TStrongObjectPtr CachedMusicSoundClass; + + TStrongObjectPtr CachedEffectsSoundClass; + + TStrongObjectPtr CachedUiSoundClass; +}; diff --git a/Source/Survival_projet/Public/SurvivalGameInstance.h b/Source/Survival_projet/Public/SurvivalGameInstance.h index 4b8ee16..b361990 100644 --- a/Source/Survival_projet/Public/SurvivalGameInstance.h +++ b/Source/Survival_projet/Public/SurvivalGameInstance.h @@ -51,6 +51,31 @@ public: UFUNCTION(BlueprintCallable, Category = "Input") 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 * se taire. @@ -208,6 +233,16 @@ private: /** Un retour au menu est deja en cours, voulu ou non. */ 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 * supprime une camera dans la map, et on retomberait sur autre chose. diff --git a/Source/Survival_projet/Public/SurvivalUserSettings.h b/Source/Survival_projet/Public/SurvivalUserSettings.h index 2dd9c82..35290e2 100644 --- a/Source/Survival_projet/Public/SurvivalUserSettings.h +++ b/Source/Survival_projet/Public/SurvivalUserSettings.h @@ -74,6 +74,23 @@ namespace SurvivalSettingsRange */ constexpr float LookMultiplierAtMin = 0.05f; 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") 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. */ UFUNCTION(BlueprintCallable, Category = "Reglages") 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 ApplyNonResolutionSettings() override; @@ -215,6 +247,45 @@ public: UFUNCTION(BlueprintCallable, Category = "Reglages|Affichage") 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: /** Echelle lisible (5 a 100), convertie a la lecture. Jamais brute. */ UPROPERTY(config) @@ -245,4 +316,24 @@ private: /** Pourcentage, converti en gamma a l'application. */ UPROPERTY(config) 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; }; diff --git a/Source/Survival_projet/Survival_projet.Build.cs b/Source/Survival_projet/Survival_projet.Build.cs index aa7aafe..e5c70a5 100644 --- a/Source/Survival_projet/Survival_projet.Build.cs +++ b/Source/Survival_projet/Survival_projet.Build.cs @@ -15,7 +15,10 @@ public class Survival_projet : ModuleRules // extinction). Passer par un evenement Blueprint marcherait aussi, mais // l'etat du feu appartient au C++ -- le Blueprint ne doit brancher que // 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 // redeclarer le chemin de la map de menu dans le C++ alors qu'il est