16 KiB
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,
constet références là où c'est correct. - Optimisé : pas de
Tickquand un délégué ou un timer suffit, pas deFindComponentni 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 :
- Écrire et compiler le C++.
- 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. - 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
& "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.dllsignifie 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
EPERMsurFpsPlayer.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 |
| Survie | SurvivalStatsComponent, SurvivalStatsWidget |
| Menu principal | MainMenuGameMode, MainMenuPlayerController, MainMenuWidget, MenuCameraSpot |
| Fabrication | CraftingRecipeDataAsset, CraftingRecipeBook, CraftingComponent, CraftingWidget, CraftingRecipeSlotWidget, CraftingIngredientRowWidget |
| É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
TArrayde 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_Fadeau ZOrder 1000. Fondre widget par widget est fragile — les cases réécrivent leur propreRenderOpacity. - Après un respawn, le pawn est un nouvel acteur : les widgets doivent se rebrancher via
BindToOwningPawn(), appelé depuisOnPossess. Même raison côté réglages : le pawn les relit à sonBeginPlayet 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 etScreenFadeComponentcontinuent (d'où sonbTickEvenWhenPaused). Pas de bouton « Quitter le jeu » : la sortie se fait depuis le menu principal. - Réglages :
USurvivalUserSettingsfait 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 seulWBP_Settingspour le menu principal et la pause — et il reçoit même le ZOrder de sa boîte de dialogue parSetDialogZOrder(): 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 seulWBP_SettingRowpour 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;UCraftingComponenttient 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 parOnCraftOverflowet 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. - La barre rapide se masque sur l'onglet Fabrication.
UInventoryScreenWidgetse contente de diffuserOnTabChanged; c'est le contrôleur qui décide, via une uniqueUpdateHotbarVisibility(). 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.
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_Inventorydevenu page d'un WidgetSwitcher. - Dans un Vertical/Horizontal Box, un enfant en
Size = Fillne 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
Filll'étire quand même. Il fautAuto+Center. - Une Border ne peint rien avec
Draw As = Imagesans texture. Pour un aplat de couleur, c'estRounded 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 Fitest le seul « keep aspect ratio » d'UMG ; un Size Box avecMin/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 desFTextetLOCTEXT. UnECraftingStation::Hands, pasAlaMain. 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 desCategoryet desFTexten 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++
Abstractpour la logique, Widget Blueprint enfant pour la mise en page. Les widgets liés utilisentmeta = (BindWidget), ouBindWidgetOptionalquand 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 → 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.