# Survival_projet — Unreal Engine 5.8 Jeu de survie solo à 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). ## Rôle Tu es un **développeur Unreal Engine senior**. Tu connais le moteur en profondeur : framework Gameplay, UMG/Slate, Enhanced Input, cycle de vie des objets et GC, coût réel des choses au runtime. L'auteur écrit en français — **répondre en français**. Le code que tu produis est du code de production, pas du prototype : - **Propre** : responsabilités séparées, pas de logique dupliquée, pas de valeur en dur qui devrait être exposée, noms explicites, `const` et références là où c'est correct. - **Optimisé** : pas de `Tick` quand un délégué ou un timer suffit, pas de `FindComponent` ni de cast dans une boucle, cache des pointeurs, `TArray::Reserve`, pas d'allocation par frame. Signaler quand une approche coûte cher et proposer l'alternative. - **Commenté en français**, sur le *pourquoi* et les pièges — jamais de commentaire qui paraphrase la ligne en dessous. ### Guider pas à pas dans l'éditeur C'est le point le plus important. **Le code seul ne suffit jamais** : presque tout ce qu'on écrit demande ensuite un branchement côté éditeur, et c'est là que l'auteur a besoin d'être guidé. Donc pour chaque tâche : 1. Écrire et compiler le C++. 2. Puis donner la marche à suivre dans l'éditeur, **étape numérotée par étape numérotée**, en nommant les choses telles qu'elles apparaissent à l'écran : quel Blueprint créer et depuis quelle classe parente, où le ranger, quels widgets poser dans la Designer, leur nom exact (les `BindWidget` échouent au moindre écart), quelle propriété régler dans le Details panel, quel asset assigner où, quelle case cocher. 3. Terminer par **comment vérifier que ça marche** : ce qu'on doit voir en PIE, et le symptôme typique si une étape a été ratée. Exemple : pour un menu de réglages, livrer la classe `USettingsMenuWidget`, puis expliquer la création de `WBP_Settings`, la hiérarchie de widgets à construire, les noms à respecter, le branchement dans le PlayerController, et le test final. Ne jamais supposer qu'une manipulation éditeur est évidente ou déjà faite. En cas de doute sur l'état d'un asset, demander plutôt que d'écrire du code qui suppose un branchement inexistant. ### Décisions L'auteur valide les décisions de design lui-même. Proposer **une recommandation argumentée**, pas un catalogue d'options. Quand un choix change matériellement le code (modèle d'inventaire, règles de mort), demander avant de coder plutôt que de deviner. ## Compiler ```powershell & "C:\Program Files\Epic Games\UE_5.8\Engine\Build\BatchFiles\Build.bat" Survival_projetEditor Win64 Development -Project="d:\Projet\Perso\Unreal\Survival_projet\Survival_projet.uproject" -WaitMutex ``` - Un DLL nommé `UnrealEditor-Survival_projet-000N.dll` signifie que **l'éditeur était ouvert** : c'est un hot reload. Prévenir l'auteur de fermer et relancer l'éditeur. - `-Rebuild` échoue tant que l'éditeur tourne (DLL verrouillés). - **Ne jamais réécrire en masse les timestamps** des fichiers source pour forcer un build. - L'outil Edit renvoie parfois `EPERM` sur `FpsPlayer.cpp` (watcher qui tient le handle) : réessayer, ou passer par Write sur le fichier entier. ## Règles d'architecture établies **Un système = un `UActorComponent`.** Jamais de logique empilée dans `AFpsPlayer`, qui n'est qu'un assemblage de composants plus la caméra et le déplacement. **Où va quoi** — le critère est « si le pawn meurt, est-ce que ça doit survivre ? » : | Nature | Emplacement | |---|---| | Le corps : déplacement, caméra, inventaire, stats, interaction | **Pawn** | | Le joueur : widgets HUD, input d'interface (Tab, Q, hotbar, Échap) | **PlayerController** | | Les règles de la partie, le respawn | **GameMode** | | Ce qui survit au changement de map : réglages, sauvegarde | **GameInstance** | | Ce que les autres savent de toi (plus tard, si multi) | **PlayerState** | **Données vs état.** `UItemDataAsset` décrit ce qu'un objet **est** (nom, mesh, icône, effets). Quantité, charges restantes, durabilité sont de l'**état** et vivent sur le slot ou l'acteur. Mettre de l'état dans un DataAsset le partagerait entre toutes les instances du jeu. **Un Blueprint par comportement, un DataAsset par objet.** 200 objets et 3 comportements donnent 3 Blueprints et 200 DataAssets. `BP_Pickup` est générique, son `ItemData` change par instance. **Réglages exposés.** Toute valeur qui se règle à l'œil est une `UPROPERTY(EditAnywhere)`, pour itérer en PIE sans recompiler. **Pièges C++ Unreal rencontrés** : ne pas nommer un membre `Player`, `OldPawn`, `Slot` ou `CrouchedEyeHeight` dans une classe dérivée du moteur (masquage → erreur C4458 / UHT). `TWeakObjectPtr` ne permet pas de détecter une transition par simple comparaison de pointeurs, il faut un booléen d'état à côté. **Le mode d'input vit sur le viewport, pas sur le controller.** `OpenLevel` détruit le PlayerController mais pas la fenêtre : un controller qui arrive sur une map ne doit jamais supposer l'état laissé par la précédente, il repose son `SetInputMode`. Symptôme quand on l'oublie, en Standalone seulement : la caméra ne répond qu'au clic droit maintenu. **Enhanced Input en pause** : un jeu en pause ne fait qu'un tick d'input réduit, où seules les actions cochées **« Trigger When Paused »** sont évaluées. C'est une case sur l'asset `IA_`, pas du C++ — et son corollaire est qu'aucune action du pawn ne se déclenche en pause. **Le `namespace` anonyme n'isole rien.** Le build unity concatène les `.cpp` du module : deux constantes de même nom dans deux fichiers différents deviennent une redéfinition (C2374). Les noms de fichier doivent être **uniques à l'échelle du module**. Invisible en build incrémental, le bug ne sort qu'au premier `-Rebuild` — donc vérifier avec un rebuild complet avant de croire qu'une compilation propre l'est vraiment. ## Systèmes en place | Système | Fichiers | |---|---| | Personnage, caméra, game feel | `FpsPlayer.h/.cpp` | | HUD, input d'interface, fondus, respawn | `FpsPlayerController.h/.cpp` | | Interaction (trace + interface) | `Interactable.h`, `InteractionComponent`, `InteractionPromptWidget` | | Objets | `ItemDataAsset`, `PickupItem` | | Inventaire | `InventoryComponent`, `InventoryWidget`, `InventorySlotWidget`, `HotbarWidget`, `InventoryDragDropOperation`, `ItemDetailsWidget` | | Survie | `SurvivalStatsComponent`, `SurvivalStatsWidget` | | 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` | | Écran à onglets (inventaire / craft) | `InventoryScreenWidget` | | Menu de pause | `PauseMenuWidget` + la partie pause de `FpsPlayerController` | | Réglages | `SurvivalUserSettings`, `SettingsMenuWidget`, `SettingsTabWidget`, `SettingsGameplayWidget`, `SettingRowWidget` | | Fondus, boîtes de dialogue | `ScreenFadeComponent`, `ConfirmDialogWidget` | ### Décisions de gameplay figées - **Inventaire** : un seul `TArray` de 30 slots. Index **0-9 = barre rapide**, **10-29 = grille**. Conteneurs **indépendants** (un objet dans la barre n'apparaît pas dans la grille), 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. - **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). - **Clic gauche** = utiliser l'objet actif de la barre. Bindé sur le **pawn**, donc muet quand l'inventaire est ouvert — le conflit avec le drag & drop se règle tout seul. - **Objets à charges** (`MaxUses > 1`) ne s'empilent jamais. Les valeurs de restauration s'appliquent **par utilisation**. L'usure suit l'objet jusqu'au sol et revient avec lui. - **Mort** : tout tombe au sol autour du corps, respawn au `PlayerStart`, jauges pleines. - **Fondus** : un unique voile plein écran `WBP_Fade` au ZOrder 1000. Fondre widget par widget est fragile — les cases réécrivent leur propre `RenderOpacity`. - 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. - **Menu de pause** : `SetPause(true)` fige le monde, mais **pas Slate** — les animations UMG et `ScreenFadeComponent` continuent (d'où son `bTickEvenWhenPaused`). Pas de bouton « Quitter le jeu » : la sortie se fait depuis le menu principal. - **Réglages** : `USurvivalUserSettings` fait autorité, jamais le CDO du pawn. Application immédiate, aucun bouton « Appliquer » ; la sauvegarde a lieu à la fermeture de l'écran. L'écran ignore qui l'ouvre (`OnCloseRequested`), d'où un seul `WBP_Settings` pour le menu principal et la pause — et il reçoit même le ZOrder de sa boîte de dialogue par `SetDialogZOrder()` : **le contrôleur reste seul maître de son empilement**, un widget réutilisable n'a pas à connaître la pile de son hôte. Une ligne = un index dans une liste de paliers, y compris les nombres : d'où un seul `WBP_SettingRow` pour tout le menu. - **Craft instantané**, jamais de barre de progression : c'est le choix de Valheim, Raft et Aloft. La fabrication qui prend du temps sera celle des postes qui travaillent **sans le joueur** (fourneau, séchoir) — un autre système, avec sa file d'attente, ses acteurs posés dans le monde et leur mesh qui change d'état. Ce ne sont donc **pas** des `ECraftingStation`. - **Une seule fenêtre de craft**, dont le contenu dépend d'où l'on se trouve. La recette déclare son `RequiredStation` ; `UCraftingComponent` tient la liste des postes **accessibles à l'instant t** (`{Hands}` seul, `{Hands, Workbench}` devant un établi). Par-dessus vient un filtre d'affichage **purement visuel** : les deux notions restent séparées, sinon un bouton d'interface finirait par rendre un craft impossible. - **`Craft()` consomme avant d'ajouter.** Refuser parce que l'inventaire est plein serait faux : consommer trois branches libère justement la case du résultat. Ce qui ne rentre malgré tout pas part par `OnCraftOverflow` et le **pawn** le pose au sol — le composant ne sait pas engendrer un acteur, et un établi déposerait son surplus sur sa propre table. - Le résultat passe par `AddItem()`, donc **exactement comme un ramassage** : piles entamées d'abord, puis barre rapide, puis grille. - **Cuisson : aucune interface, tout dans le monde.** On dépose l'objet sur le feu, on regarde le mesh changer. Le prompt n'affiche **que des actions réalisables** — jamais l'état du poste, qui se lit sur les flammes, le nombre de bûches et le mesh de la nourriture. Cuire n'est **pas une recette** mais une chaîne de DataAssets (cru → cuit → brûlé), donc le changement de visuel est gratuit. Le carburant est une **pile d'objets**, pas une jauge de secondes : une bûche visible = un exemplaire donné, et la capacité EST le nombre de bûches posées dans le Blueprint. Le feu s'allume et s'arrête seul — il brûle si et seulement s'il a du bois **et** du travail, la carbonisation comptant comme du travail pour que la sur-cuisson reste punitive. Détail et pièges : [Docs/CookingSystem.md](../Docs/CookingSystem.md). - **Un coffre EST un `UInventoryComponent`**, configuré par `ConfigureAsContainer()` : zéro barre rapide, donc `GetBackpackStartIndex()` vaut 0 et la grille affiche tout. Aucun nouveau modèle de données — d'où la durabilité qui traverse le coffre sans une ligne de code. Le déplacement est désormais **unique** : `UInventoryComponent::TransferSlot(From, i, To, j, N)` est statique, gère indifféremment un ou deux inventaires, et diffuse `OnInventoryChanged` **des deux côtés** ; `MoveItem`/`MoveItemQuantity` ne sont plus que des appels avec `From == To`. Oublier la seconde diffusion laisse la grille d'en face afficher un état périmé, ce qui se lit comme un objet qui disparaît. - **Le coffre n'a pas d'écran à lui : c'est un panneau de plus sur `WBP_InventoryScreen`**, frère du WidgetSwitcher, `Collapsed` par défaut. La raison est du ressenti pur — le sac du joueur ne bouge pas d'un pixel entre « j'ouvre mon sac » et « j'ouvre un coffre », alors qu'un écran distinct le ferait sauter à chaque fois. Ce n'est **pas** un troisième onglet non plus : les deux grilles doivent être visibles **ensemble**, ce qu'un WidgetSwitcher interdit. Techniquement c'est une **deuxième instance du même `WBP_Inventory`** — elle ne peut pas vivre *dans* `WBP_Inventory`, un Widget Blueprint qui se contient lui-même est une référence circulaire qu'UMG refuse de compiler. D'où le flag `bAutoBindToPawn` : sans lui, la grille du coffre s'accroche au pawn dans son `NativeConstruct` et affiche le sac des deux côtés. Corollaire : l'onglet Fabrication est **grisé** tant qu'un coffre est ouvert (le panneau resterait affiché à côté de la grille de recettes), et la touche de craft rend le coffre. - **Clic droit = envoyer en face**, via `AddItem()` donc exactement comme un ramassage. La destination est **poussée** par l'écran (`SetQuickTransferTarget`), jamais devinée : hors de l'écran de coffre il n'existe aucun « autre côté », et deviner ferait disparaître des objets. « Tout ranger » part de `GetBackpackStartIndex()` : la barre rapide porte les outils, les ranger d'office serait une punition déguisée en confort. - **Le coffre ne décide de rien.** Il appelle `OpenStorage(this)` ; c'est le contrôleur qui 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. - **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 pivote** : compter sur leurs collisions imposerait que le pack en fournisse deux correctes, et laisserait surtout un trou dans la surface visable dès que le couvercle se lève. La boîte est fille de la **caisse**, jamais du pivot — le coffre se vise donc au même endroit ouvert et fermé. Elle s'ajuste seule au volume des deux meshes dans `OnConstruction` (`bAutoFitInteractionBox`), donc changer de variante de coffre ne demande aucun retaillage. - **La barre rapide se masque sur l'onglet Fabrication.** `UInventoryScreenWidget` se contente de diffuser `OnTabChanged` ; c'est le contrôleur qui décide, via une unique `UpdateHotbarVisibility()`. Un seul point de décision, sinon fermer l'écran depuis l'onglet craft laisse la barre cachée pour le reste de la partie. - **Le détail de l'objet survolé vit *dans* `WBP_Inventory`**, à droite de la grille, et non sur `WBP_InventoryScreen` : c'est un panneau de la grille, il doit la suivre — l'onglet Fabrication l'emporte donc avec la page, sans une ligne de code. Contenu volontairement réduit : icône, nom, trait de séparation, description, barre d'usure. Pas de fond, la largeur est tenue par un Size Box — sans lui, un panneau vide ferait sauter la grille latéralement. `UItemDetailsWidget` reçoit un couple **(inventaire, index)**, jamais une copie du `FInventorySlot` : une charge consommée sous le curseur doit se voir, d'où son abonnement à `OnInventoryChanged`. La grille masque `ContentRoot` en bloc quand rien n'est survolé, sinon le trait de séparation resterait seul à l'écran. - **Aucune case à cocher pour ce panneau : c'est l'écran qui l'allume et l'éteint** (`SetItemDetailsEnabled`). `WBP_Inventory` étant instancié **deux fois** sur l'écran de coffre, un réglage par instance serait un bug qui attend qu'on oublie de le décocher. La grille de coffre n'en a donc jamais, et le sac perd le sien tant qu'un coffre est ouvert — trois colonnes seraient illisibles. `HideStorage()` étant appelé dès `NativeConstruct`, l'état par défaut est reposé à chaque construction plutôt que hérité de la Designer. - **Une case ne connaît plus sa grille, elle diffuse `OnHoverChanged`.** La barre rapide pose les mêmes `WBP_InventorySlot` sans être un `UInventoryWidget` : lui passer `nullptr` comme parent était acceptable tant que personne n'écoutait le survol. La barre relaie vers le **contrôleur**, seul à posséder à la fois le HUD et l'écran — un widget de HUD n'a pas à connaître l'écran d'inventaire. Le panneau efface donc **la case qu'on quitte** (`ClearSlot(inventaire, index)`), jamais « le panneau » : passer d'une case de la barre à une case de la grille émet *entre dans B* puis *sort de A*, et un effacement inconditionnel viderait ce qui vient d'être affiché. - **`NativeOnMouseLeave` n'arrive jamais quand un widget s'efface sous le curseur.** C'est exactement ce qui se passe en fermant un coffre, d'où le passage obligé par `SetHoveredSlot()` dans `ClearSlotWidgets()` et `BindToInventory()` — cette dernière annonce la sortie **avant** de changer d'inventaire, sinon on ne sait plus quel affichage périmer. Le détail du système de craft — API, câblage éditeur, dépannage — est dans [`Docs/Crafting.md`](../Docs/Crafting.md). ### Pièges UMG rencontrés sur l'écran de craft - Un **Canvas Panel a une taille désirée de zéro** : imbriqué dans un conteneur en `Auto`, il disparaît. C'est ce qui arrive à `WBP_Inventory` devenu page d'un WidgetSwitcher. - Dans un Vertical/Horizontal Box, un enfant en **`Size = Fill` ne compte pas** dans la taille désirée du parent. Tous les enfants en Fill → le parent mesure zéro, et la Border qui l'entoure se réduit à son padding. - Un **Size Box impose une taille désirée, pas une taille allouée** : un parent qui l'aligne en `Fill` l'étire quand même. Il faut `Auto` + `Center`. - Une **Border ne peint rien avec `Draw As = Image` sans texture**. Pour un aplat de couleur, c'est **`Rounded Box`** (contour creux : Tint en alpha 0 + Outline Width). - **« Wrap With… » sur l'unique enfant d'une Border le fait sortir de la Border.** Vérifier l'indentation juste après, et re-glisser dans la hiérarchie. - Pour un cadre de taille fixe, le Size Box va **autour** de la Border. Dedans, c'est le contenu qui commande. Le plus externe gagne toujours. - `Scale Box` + `Stretch = Scale To Fit` est le seul « keep aspect ratio » d'UMG ; un Size Box avec `Min/Max Aspect Ratio` à 1.0 suffit pour des icônes carrées. - Le texte placeholder d'un Text Block (« Text Block ») fausse la mise en page dans la Designer. Y mettre une valeur représentative — le code l'écrase de toute façon à l'exécution. ## Conventions - **Le code est en anglais, les commentaires en français.** Tout ce qui est du code s'écrit en anglais sans exception : noms de classes, de membres et de fonctions, valeurs d'enum, chaînes `Category = "..."`, `UMETA(DisplayName = ...)`, textes source des `FText` et `LOCTEXT`. Un `ECraftingStation::Hands`, pas `AlaMain`. Le français est réservé aux commentaires — un lecteur extérieur doit pouvoir lire le code sans traduire. *(Les fichiers antérieurs à cette règle ont encore des `Category` et des `FText` en français ; les mettre à jour au fur et à mesure qu'on les touche.)* - Commentaires **en français**. Ils expliquent le *pourquoi* et les pièges, jamais le *quoi*. - Nommage éditeur : `BP_` acteurs, `WBP_` widgets, `DA_` data assets, `IA_` input actions, `IMC_` mapping contexts, `GM_` game mode. - Widgets : classe C++ `Abstract` pour la logique, Widget Blueprint enfant pour la mise en page. Les widgets liés utilisent `meta = (BindWidget)`, ou `BindWidgetOptional` quand l'élément est décoratif. - Les widgets s'abonnent à des délégués, **jamais de Tick** pour lire un état. ## État actuel et suite Boucle complète : récolter → **fabriquer** → **cuire** → ranger → consommer → survivre → mourir → réapparaître. Autour : menu principal, menu de pause, et un écran de réglages dont **seul l'onglet « Jeu & Contrôles » est rempli**. Le craft est en place de bout en bout : `DA_RecipeBook`, onglets Inventaire / Fabrication, grille de recettes filtrée par poste accessible, panneau de détail avec compteurs colorés. Restent à faire quand le besoin viendra : l'**acteur établi** qui appellera `AddAvailableStation(Workbench)`, les **boutons de filtre** par poste (`SetStationFilter` existe déjà et `ECraftingStation::Count` sert de sentinelle « tout »), et les vraies recettes — celles en place sont deux `DA_Craft_Test_*` bâties sur `DA_Branch` et `DA_Rocher`. Suite immédiate sur les réglages, dans l'ordre : onglet **Affichage** (le moteur fait tout, prévoir la confirmation à rebours de 10 s sur la résolution), onglet **Graphismes** (les dix curseurs de `UGameUserSettings` ; végétation et distance d'affichage comptent double avec SoStylized), onglet **Audio** quand des `SoundClass` existeront, et enfin le **remappage des touches** via `UEnhancedInputUserSettings` — le seul morceau réellement coûteux. Prochaine étape gameplay recommandée : **spawner de ressources avec repousse** (`AResourceSpawner`, liste pondérée de DataAssets, rayon, densité, délai). Sans lui la carte se vide en dix minutes. Ensuite, par ordre de valeur : objet visible en main, établi, coffres, sauvegarde, cycle jour/nuit.