(Feat) Add footsteps
This commit is contained in:
+100
-7
@@ -1,6 +1,6 @@
|
||||
# Survival_projet — Unreal Engine 5.8
|
||||
|
||||
Jeu de survie solo à la première personne, gameplay écrit en **C++**, Blueprints réservés au
|
||||
Jeu de survie coop en ligne via steam p2p à la première personne, gameplay écrit en **C++**, Blueprints réservés au
|
||||
câblage d'assets. Inspirations assumées : Raft, Aloft, Valheim, esthétique solarpunk.
|
||||
Assets d'environnement : pack **SoStylized** (sa végétation n'est pas à l'échelle humaine).
|
||||
|
||||
@@ -124,12 +124,13 @@ qu'une compilation propre l'est vraiment.
|
||||
| Menu principal | `MainMenuGameMode`, `MainMenuPlayerController`, `MainMenuWidget`, `MenuCameraSpot` |
|
||||
| Fabrication | `CraftingRecipeDataAsset`, `CraftingRecipeBook`, `CraftingComponent`, `CraftingWidget`, `CraftingRecipeSlotWidget`, `CraftingIngredientRowWidget` |
|
||||
| Cuisson | `CookingStation` + bloc *Cooking* / *Fuel* de `ItemDataAsset` — **doc complète : [Docs/CookingSystem.md](../Docs/CookingSystem.md)** |
|
||||
| Coffres | `StorageContainer`, `StorageScreenWidget` + `UInventoryComponent::TransferSlot` / `TransferAllTo` |
|
||||
| Coffres | `StorageContainer` + le panneau de coffre d'`InventoryScreenWidget` et `UInventoryComponent::TransferSlot` / `TransferAllTo` — **doc complète : [Docs/Storage.md](../Docs/Storage.md)** |
|
||||
| É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` |
|
||||
| Bruits de pas | `FootstepComponent`, `FootstepSoundsDataAsset` + les `PhysicalSurfaces` de `DefaultEngine.ini` |
|
||||
| Fondus, boîtes de dialogue | `ScreenFadeComponent`, `ConfirmDialogWidget` |
|
||||
| Coop Steam (sessions) | `SessionSubsystem`, `SurvivalGameMode`, `SurvivalGameState`, `SurvivalPlayerState` |
|
||||
| Pseudo au-dessus des joueurs | `PlayerNameplateComponent`, `PlayerNameplateWidget` |
|
||||
@@ -487,6 +488,57 @@ du joueur dans lui-même.
|
||||
(`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.
|
||||
|
||||
### Bruits de pas — la distance, pas les AnimNotify
|
||||
|
||||
- **Le pas est déclenché par la DISTANCE parcourue**, jamais par un notify d'animation.
|
||||
`UFootstepComponent` accumule le déplacement horizontal et sonne tous les `StrideLength` cm.
|
||||
Trois raisons : le corps 3e personne du joueur local est en `bOwnerNoSee`, les notifies posés
|
||||
dans un **BlendSpace** se déclenchent selon l'échantillon dominant et sautent ou doublent un pas
|
||||
au passage marche → course, et les bras FPS sont sur `BasePose_Skeleton`, qui n'a rien à voir
|
||||
avec `SK_BodyA` — aucun notify ne pourrait servir aux deux.
|
||||
- **`StrideLength` se calcule, il ne se devine pas** : `Vitesse / (2 × StrideFrequency)` du profil
|
||||
correspondant de `UHeadBobComponent`, le facteur 2 parce qu'une foulée compte deux pas. D'où
|
||||
143 / 182 / 210 cm pour accroupi / marche / sprint. Changer `WalkSpeed` ou `SprintSpeed` sans
|
||||
retoucher ces valeurs désynchronise le son de la tête, et **ça s'entend avant de se voir**.
|
||||
- **Rien n'est répliqué**, exactement comme `UPlayerBodyAnimInstance` : chaque machine calcule les
|
||||
pas de tous les pawns qu'elle connaît, à partir de la vitesse et du mode de déplacement déjà
|
||||
synchronisés. Un multicast par pas coûterait trois messages par seconde et par joueur pour un
|
||||
résultat identique.
|
||||
- **Saut et réception passent par `MovementModeChangedDelegate`, surtout pas par
|
||||
`ACharacter::Landed` / `OnJumped`.** `Landed` vient de `ProcessLanded`, qui ne tourne que là où
|
||||
le mouvement est réellement simulé, et `OnJumped` que là où l'input est lu : sur le pawn d'un ami
|
||||
vu depuis notre machine, ni l'un ni l'autre n'existe. Le **mode de déplacement**, lui, est
|
||||
répliqué, donc son délégué est diffusé partout. Et c'est le **signe de `Velocity.Z`** qui
|
||||
distingue un saut d'une sortie de rebord, comme dans l'AnimBP du corps.
|
||||
- **La vitesse de chute s'échantillonne PENDANT la chute** : au moment où le mode repasse à
|
||||
`Walking`, le moteur a déjà remis `Velocity.Z` à zéro. Le cas où le tick à 20 Hz n'a pas eu le
|
||||
temps de tourner est précisément celui d'une chute négligeable, qu'on veut de toute façon muette.
|
||||
- **`bTraceComplex = true` n'est pas négociable.** Un `PhysicalMaterial` posé sur un **matériau**
|
||||
n'est rendu que par un trace per-poly ; le trace simple remonte celui du `BodySetup` du mesh,
|
||||
presque toujours vide. Symptôme d'un décochage : tout le décor sonne sur `DefaultSurface`, ce
|
||||
qu'on lit comme « la table n'est pas remplie ». Coût réel : deux à trois traces par seconde et
|
||||
par personnage, jamais un par frame — le tick tourne à **20 Hz** et ne fait qu'une soustraction
|
||||
de vecteurs, le trace n'a lieu qu'au moment du pas.
|
||||
- **Sur un Material INSTANCE, le champ *Phys Material* a une case à cocher devant lui, et elle
|
||||
compte.** `UMaterialInstance::GetPhysicalMaterial()` teste `bOverridePhysMaterial` **avant** de
|
||||
regarder le pointeur : décochée, l'instance hérite de son parent quoi qu'on ait glissé dans le
|
||||
champ ; cochée mais vide, elle retombe sur `DefaultPhysMaterial` et **coupe** l'héritage.
|
||||
Corollaire précieux pour SoStylized, dont les 398 instances descendent d'une poignée de parents :
|
||||
taguer `MI_Rock`, `MI_Grass`, `MI_Snow`… suffit à couvrir toute leur descendance, et l'on ne
|
||||
redescend dans un enfant que pour l'exception.
|
||||
- **L'ordre des `PhysicalSurfaces` dans `DefaultEngine.ini` EST le contrat.** Le moteur ne stocke
|
||||
que le numéro, jamais le nom : insérer une matière au milieu renumérote tout ce qui suit et les
|
||||
`PM_` existants pointent silencieusement ailleurs. **Toute nouvelle surface s'ajoute à la fin.**
|
||||
- Le pas du joueur local part en **2D** (`bPlayLocalStepsIn2D`), celui des autres en 3D à leurs
|
||||
pieds : joué en 3D depuis ses propres chevilles, le pas local passe par l'atténuation et sonne
|
||||
creux alors qu'il devrait être le son le plus présent du mixage. Les cues du pack n'ont pas de
|
||||
`SoundClass`, donc les deux chemins tombent sur `SC_SFX` — le curseur *Effets* les couvre sans
|
||||
qu'on ait rien à taguer.
|
||||
- Les noms des surfaces décrivent le **monde**, pas le pack : `Grass` existe alors que le pack
|
||||
livre *Leaves*, `Stone` alors qu'il livre *Concrete*. C'est `DA_Footsteps` qui fait la
|
||||
traduction, et c'est sa raison d'être — changer de pack de sons ne doit toucher ni la config ni
|
||||
les `PM_`.
|
||||
|
||||
### Décisions de gameplay figées
|
||||
|
||||
- **Inventaire** : un seul `TArray` de 30 slots. Index **0-9 = barre rapide**, **10-29 = grille**.
|
||||
@@ -494,6 +546,20 @@ du joueur dans lui-même.
|
||||
tableau uniquement pour que le glisser-déposer n'ait aucune conversion d'index à faire.
|
||||
- **Ramassage** : complète les piles existantes partout, puis ouvre un slot dans la **barre en
|
||||
priorité**, puis déborde dans la grille.
|
||||
- **Le volume visable d'un `APickupItem` est une BOÎTE ajustée au mesh, jamais une sphère.** Un
|
||||
seul `BP_Pickup` sert des centaines d'objets : sa taille se calcule à partir de
|
||||
`UStaticMesh::GetBoundingBox()` dans `FitInteractionBox()`, appelée par `ApplyItemVisuals()`
|
||||
donc par `OnConstruction` **et** `OnRep_ItemData`. Trois raisons de ne pas revenir à la sphère :
|
||||
son rayon englobant vaut la **demi-diagonale**, ce qui est le pire cas exact pour les objets
|
||||
allongés du jeu (une branche de 120×20×15 donnait 62 cm de rayon, vingt-sept fois son volume) ;
|
||||
elle ne peut pas exprimer une orientation, là où la boîte tourne avec l'acteur ; et le symptôme
|
||||
n'est pas cosmétique — deux pickups côte à côte voyaient leurs volumes se chevaucher, et le
|
||||
trace retenait le **premier touché** plutôt que l'objet visé. La marge (`InteractionBoxPadding`)
|
||||
est **additive en cm et jamais multiplicative** : un facteur appliqué à la demi-épaisseur d'une
|
||||
branche n'ajoute rien tout en rallongeant son grand axe. Deux pièges partagés avec
|
||||
`AStorageContainer::FitInteractionBox` : lire les bounds de l'**asset** et pas `Mesh->Bounds`,
|
||||
déjà multipliées par l'échelle que `SetBoxExtent` remultiplie ; et recentrer sur
|
||||
`GetCenter()`, le pivot d'un mesh de pack étant presque toujours au sol.
|
||||
- **Glisser** : normal = pile entière, **Maj** = moitié arrondie au supérieur, **Ctrl** = un seul.
|
||||
Lâcher hors de la grille jette au sol ; lâcher entre deux cases est absorbé (jamais de perte
|
||||
accidentelle).
|
||||
@@ -507,8 +573,26 @@ du joueur dans lui-même.
|
||||
- Après un respawn, le pawn est un **nouvel acteur** : les widgets doivent se rebrancher via
|
||||
`BindToOwningPawn()`, appelé depuis `OnPossess`. Même raison côté réglages : le pawn les
|
||||
**relit** à son `BeginPlay` et s'abonne à `OnSurvivalSettingsApplied`.
|
||||
- **Échap** a une priorité explicite dans `HandlePauseInput()` : boîte de confirmation →
|
||||
réglages → inventaire → pause. Sans cet ordre, deux écrans se retrouvent ouverts en même temps.
|
||||
- **Échap** a une priorité explicite dans `HandleEscapeInput()` : boîte de confirmation →
|
||||
réglages → coffre → inventaire → pause. Sans cet ordre, deux écrans se retrouvent ouverts en
|
||||
même temps.
|
||||
- **Un widget UMG qui a le focus clavier COUPE la route des touches vers Enhanced Input.** Les
|
||||
widgets vivent dans le `SGameLayerManager`, **frère** du `SViewport` et pas son enfant : une
|
||||
touche remonte les parents du widget focalisé et n'atteint jamais le viewport. Or un `UButton`
|
||||
prend le focus au clic. Symptôme exact rencontré : Échap refermait l'inventaire (que des cases,
|
||||
rien de focusable) mais plus le menu de pause dès qu'on avait cliqué dedans — et jamais après un
|
||||
aller-retour dans les réglages, qui prennent le focus (`SetKeyboardFocus`) sans le rendre.
|
||||
La parade est donc double, et les deux moitiés comptent : tout écran à boutons est `Focusable`
|
||||
et relaie Échap par `NativeOnKeyDown` vers `HandleEscapeInput()` — un seul point d'entrée, donc
|
||||
la priorité ci-dessus reste vraie quel que soit le chemin — **et** l'écran qui redevient la
|
||||
couche active **reprend** le focus (`UPauseMenuWidget::SetInteractive(true)`). Corollaire
|
||||
assumé : menu de pause ouvert, Tab et les touches de barre rapide ne répondent plus.
|
||||
- **Dans la map du menu, `AMainMenuPlayerController` ne binde AUCUNE action d'input.** Tout Échap
|
||||
qui y fonctionne passe donc par le `NativeOnKeyDown` d'un widget — il n'existe pas d'autre
|
||||
chemin, et un écran qui l'oublie n'a simplement pas de touche Retour. C'était le cas de
|
||||
`UCharacterCustomizationWidget`. Son `SetWidgetToFocus()` côté contrôleur ne suffisait pas non
|
||||
plus : **Slate refuse de focaliser un widget qui ne déclare pas supporter le focus clavier**,
|
||||
donc l'appel ne faisait rien tant que `SetIsFocusable(true)` manquait.
|
||||
- **Menu de pause : il ne met plus rien en pause.** `SetPause()` a disparu du projet. En coop la
|
||||
pause est impossible par nature — côté client l'appel est ignoré (elle appartient à l'autorité),
|
||||
côté hôte elle figerait le monde des quatre joueurs parce qu'un seul a ouvert son menu. Le monde
|
||||
@@ -576,9 +660,14 @@ du joueur dans lui-même.
|
||||
referme le couvercle — par Échap, par la mort, ou parce que le joueur s'est éloigné
|
||||
(`MaxStorageDistance`, surveillé par un timer à 0,25 s, jamais un Tick). Un coffre qui
|
||||
déciderait seul finirait ouvert dans la moitié de ces cas.
|
||||
- **`LidPivot`**, un `SceneComponent` vide entre la caisse et le couvercle : le pivot de
|
||||
`SM_Chest_Top` est celui du pack, presque jamais la charnière. Sans lui le couvercle tourne
|
||||
autour de son centre et traverse la caisse. Le coffre ne tick **que** pendant l'animation.
|
||||
- **Le couvercle tourne sur le pivot de son propre mesh.** `LidMesh` est enfant direct de la
|
||||
caisse et c'est lui qu'on fait tourner : un `UStaticMeshComponent` pivote toujours autour de
|
||||
l'origine de son mesh, donc la charnière est celle que le pack a donnée à `SM_Chest_Top`, sans
|
||||
composant intermédiaire à placer à la main. *(Un `SceneComponent` `LidPivot` a existé pour
|
||||
découpler les deux ; il a été retiré — un pivot vide laissé à `(0,0,0)` fait tourner le
|
||||
couvercle autour du centre de la caisse, et c'était le symptôme.)* Si un jour un pack pose ce
|
||||
pivot au centre du couvercle, on le corrige **dans l'asset** (Modeling Mode > XForm > Edit
|
||||
Pivot), pas dans le code. Le coffre ne tick **que** pendant l'animation.
|
||||
- **Toute la collision du coffre tient dans une `UBoxComponent`, les meshes n'en portent
|
||||
aucune.** C'est la seule exception à la règle « le mesh EST la surface d'interaction » posée
|
||||
par `ACraftingStation`, et elle vient de ce qu'un coffre est en **deux morceaux dont un
|
||||
@@ -675,6 +764,10 @@ Les vraies recettes restent à écrire : celles en place sont deux `DA_Craft_Tes
|
||||
|
||||
### Chantier en cours : customisation de personnage
|
||||
|
||||
**Doc complète : [Docs/CharacterCustomization.md](../Docs/CharacterCustomization.md)** — deux
|
||||
moitiés séparées, une pour l'artiste (ajouter une coiffure, une couleur, un corps, sans code) et
|
||||
une pour le développeur (architecture, réseau, pièges du moteur).
|
||||
|
||||
Découpé en quatre lots. **Le lot 1 est fait** : le pawn a enfin un corps visible et animé (voir
|
||||
*Le corps vu par les autres* plus haut) — sans lui il n'y avait rien à customiser, et les autres
|
||||
joueurs voyaient un pseudo flotter au-dessus du vide.
|
||||
|
||||
Reference in New Issue
Block a user