(Feat) Add footsteps
This commit is contained in:
@@ -0,0 +1,405 @@
|
||||
# Customisation de personnage
|
||||
|
||||
Le joueur choisit sa silhouette, son visage, sa coiffure et ses couleurs avant
|
||||
de lancer une partie. Les trois autres joueurs le voient tel qu'il s'est fait.
|
||||
|
||||
Ce document a **deux moitiés**, à lire séparément :
|
||||
|
||||
- **Partie A — pour l'artiste** : ajouter une coiffure, une couleur, un corps.
|
||||
Aucune ligne de code, aucune recompilation. Commence au §1.
|
||||
- **Partie B — pour le développeur** : architecture, réseau, pièges du moteur.
|
||||
Commence au §6.
|
||||
|
||||
---
|
||||
|
||||
# PARTIE A — Ajouter du contenu
|
||||
|
||||
## 1. Le principe en une phrase
|
||||
|
||||
**Tout passe par un seul asset : `DA_CharacterParts`.** C'est la table qui dit
|
||||
« l'option n° 3 de la catégorie Cheveux, c'est ce mesh-là ». Le code ne connaît
|
||||
que des numéros ; c'est le catalogue qui leur donne un sens.
|
||||
|
||||
Conséquence directe : **ajouter du contenu ne demande jamais de toucher au
|
||||
code**, et jamais de fermer l'éditeur.
|
||||
|
||||
```
|
||||
DA_CharacterParts
|
||||
├── Body Sets une entrée par silhouette (A, B, …)
|
||||
│ ├── Body Meshes les morceaux du corps
|
||||
│ ├── Anim Class l'AnimBP qui l'anime
|
||||
│ ├── Heads ┐
|
||||
│ ├── Hairstyles │ les listes d'options,
|
||||
│ ├── Eyebrows │ propres à CETTE silhouette
|
||||
│ ├── Beards │
|
||||
│ └── Mustaches ┘
|
||||
└── Palettes partagées par toutes les silhouettes
|
||||
├── Skin Colors
|
||||
├── Hair Colors
|
||||
├── Eye Colors
|
||||
├── Underwear Colors
|
||||
└── Eye Materials
|
||||
```
|
||||
|
||||
## 2. Ajouter une coiffure
|
||||
|
||||
1. Ouvre `Content/Game/Data/Character/DA_CharacterParts`
|
||||
2. Déplie **Body Sets** → l'entrée du corps concerné (`[0]` = A, `[1]` = B)
|
||||
3. Déplie **Hairstyles** → clique le **+**
|
||||
4. Dans la nouvelle entrée :
|
||||
- **Mesh** = ton `SKM_…`
|
||||
- **Icon** = la vignette (facultative, voir §5)
|
||||
5. Sauvegarde
|
||||
|
||||
C'est tout. La coiffure apparaît dans l'écran au prochain lancement.
|
||||
|
||||
> **L'ORDRE DE LA LISTE EST LE CONTRAT.** Un index est ce qui part sur le réseau
|
||||
> et ce qui se sauvegarde. **Insérer une entrée au milieu décale tout ce qui
|
||||
> suit** : les personnages déjà créés changeront de coiffure sans prévenir.
|
||||
> **Ajoute toujours à la fin.**
|
||||
|
||||
Même procédé pour **Heads**, **Eyebrows**, **Beards**, **Mustaches**.
|
||||
|
||||
## 3. Ajouter une couleur
|
||||
|
||||
1. Déplie **Palettes** → la palette voulue (`Skin Colors`, `Hair Colors`…)
|
||||
2. Clique le **+**
|
||||
3. Double-clique la case de couleur → dans le picker, colle une valeur dans le
|
||||
champ **Hex sRGB** (le plus fiable) ou règle R/G/B à la main
|
||||
4. Sauvegarde
|
||||
|
||||
Les palettes sont **partagées par toutes les silhouettes** : une teinte de peau
|
||||
ajoutée profite à A comme à B. Deux nuanciers à tenir à jour divergeraient au
|
||||
premier ajout.
|
||||
|
||||
> Même règle d'ordre qu'au §2 : **on ajoute à la fin**.
|
||||
|
||||
### Ce que chaque palette pilote
|
||||
|
||||
| Palette | Ce qu'elle teint |
|
||||
|---|---|
|
||||
| **Skin Colors** | le corps **et** le visage, ensemble |
|
||||
| **Hair Colors** | cheveux, **barbe, moustache et sourcils** en même temps |
|
||||
| **Eye Colors** | l'iris |
|
||||
| **Underwear Colors** | le sous-vêtement |
|
||||
|
||||
Une barre de couleur de cheveux qui ne teindrait pas la barbe donnerait un
|
||||
personnage incohérent — c'est voulu qu'elles suivent.
|
||||
|
||||
## 4. Ajouter une silhouette
|
||||
|
||||
Plus lourd, mais toujours sans code.
|
||||
|
||||
1. **Body Sets** → **+** → une nouvelle entrée
|
||||
2. **Body Meshes** → ajoute ton `SKM_` de corps (une seule case aujourd'hui)
|
||||
3. **Anim Class** → l'AnimBP à utiliser
|
||||
|
||||
> **Tu peux réutiliser `ABP_PlayerBody_A`** si ton nouveau corps partage les
|
||||
> mêmes **noms d'os**. Le moteur valide la compatibilité par les noms, pas par
|
||||
> l'identité du squelette. C'est ce qui permet à Body A et Body B de partager un
|
||||
> seul AnimBP : une correction profite aux deux.
|
||||
|
||||
4. Remplis **Heads**, **Hairstyles**, **Eyebrows** — ces listes appartiennent à
|
||||
la silhouette
|
||||
5. Laisse **Beards** et **Mustaches** vides si elle n'en porte pas : les
|
||||
catégories **disparaissent d'elles-mêmes** de la colonne de l'écran
|
||||
|
||||
## 5. Les vignettes
|
||||
|
||||
Le champ **Icon** est un `UTexture2D`, exactement comme l'icône d'un objet
|
||||
d'inventaire.
|
||||
|
||||
**Il est facultatif.** Une entrée sans vignette affiche son **numéro** dans la
|
||||
case — l'option reste choisissable. C'est délibéré : le catalogue doit rester
|
||||
utilisable avant que les trente-six vignettes existent, et le moyen le plus
|
||||
simple de les fabriquer est justement de capturer l'écran de customisation une
|
||||
fois qu'il tourne.
|
||||
|
||||
## 6. Le décor de l'écran
|
||||
|
||||
Le personnage montré pendant la customisation est un acteur posé dans
|
||||
`MenuScene` : **`BP_CharacterPreview`**. Trois choses comptent quand tu le
|
||||
places :
|
||||
|
||||
- **Hors du champ des `MenuCameraSpot`**, sinon il apparaît en arrière-plan du
|
||||
menu principal
|
||||
- **Devant un fond qui tient debout** — c'est ce qu'on voit derrière lui pendant
|
||||
toute la customisation
|
||||
- **Éclairé de face.** On passe l'essentiel du temps en gros plan sur le
|
||||
visage ; à contre-jour, les teints de peau deviennent illisibles et c'est
|
||||
précisément ce que le joueur règle
|
||||
|
||||
### Les deux cadrages
|
||||
|
||||
`FullBodyViewSpot` et `FaceViewSpot` sont deux repères vides que tu **déplaces à
|
||||
la souris** dans le viewport du Blueprint. La caméra glisse de l'un à l'autre
|
||||
selon la catégorie ouverte :
|
||||
|
||||
| Catégorie ouverte | Cadrage |
|
||||
|---|---|
|
||||
| Silhouette, Teint, Sous-vêtement | `FullBodyViewSpot` |
|
||||
| tout le reste | `FaceViewSpot` |
|
||||
|
||||
Pense à **décaler le personnage vers la droite** de l'image : la colonne d'UI
|
||||
occupe la gauche.
|
||||
|
||||
---
|
||||
|
||||
# PARTIE B — Architecture
|
||||
|
||||
## 7. Vue d'ensemble
|
||||
|
||||
| Fichier | Rôle |
|
||||
|---|---|
|
||||
| `CharacterAppearanceTypes.h` | `FCharacterAppearance` (11 octets), les enums, `FCharacterPartEntry` |
|
||||
| `CharacterPartsDataAsset` | le catalogue : index → assets, palettes, noms de paramètres matériau |
|
||||
| `CharacterAppearanceComponent` | monte les pièces, pose les couleurs. Sur le pawn **et** sur le mannequin |
|
||||
| `CharacterPreviewActor` | le mannequin du menu : mesh + caméra + cadrages |
|
||||
| `CharacterCustomizationWidget` | l'écran, la colonne, la grille |
|
||||
| `CustomizationCategoryWidget` | une rangée de la colonne |
|
||||
| `CustomizationOptionWidget` | une case : vignette **ou** pastille |
|
||||
| `SurvivalPlayerState` | porte l'apparence répliquée |
|
||||
| `SurvivalUserSettings` | la persiste dans le `.ini` |
|
||||
|
||||
## 8. `FCharacterAppearance` — pourquoi des index
|
||||
|
||||
11 octets, **que des index**, jamais de `TSoftObjectPtr` ni de chemin d'asset.
|
||||
Deux raisons qui vont dans le même sens :
|
||||
|
||||
- un chemin coûte une chaîne à chaque réplication, un index tient dans un octet ;
|
||||
- un index se **valide** contre le catalogue, alors qu'un chemin envoyé par un
|
||||
client pourrait désigner n'importe quel asset du jeu.
|
||||
|
||||
Bénéfice collatéral : la struct se sérialise telle quelle dans le `.ini`
|
||||
(`CharacterAppearance=(BodyType=BodyA,HeadIndex=0,…)`). Une apparence faite de
|
||||
références d'assets aurait donné une douzaine de chemins à écrire, qu'une seule
|
||||
renommée aurait cassés silencieusement.
|
||||
|
||||
`NoPart` vaut **255** : c'est « aucune pièce », pour une barbe rasée.
|
||||
|
||||
## 9. Le montage — `UCharacterAppearanceComponent`
|
||||
|
||||
Un `USkeletalMeshComponent` par catégorie, enfant du mesh porteur, en **Leader
|
||||
Pose**. Un seul mesh porte l'animation, les autres recopient sa pose **par nom
|
||||
d'os** — ce qui marche même entre squelettes distincts.
|
||||
|
||||
Le composant recopie sur chaque pièce le `bOwnerNoSee` et le `bCastHiddenShadow`
|
||||
du porteur. C'est ce qui fait que le **mannequin du menu est visible** sans une
|
||||
ligne de code de plus, alors que le corps du joueur reste invisible à son
|
||||
propriétaire.
|
||||
|
||||
### Le corps se monte depuis une LISTE
|
||||
|
||||
`BodyMeshes` est un `TArray` même s'il ne contient qu'une case. Le pack livre
|
||||
exprès `SKM_BodyA_torso`, `_arms`, `_legs_01/02/03`, `_hands`, `_feet` : le jour
|
||||
où une armure devra cacher les segments qu'elle recouvre, la case 0 deviendra
|
||||
`SKM_BodyA_empty` — porteur d'animation invisible — et les huit segments
|
||||
suivront derrière, **sans que ce code change**.
|
||||
|
||||
> **Contrainte à vérifier avant tout achat de pack d'armure : il doit être
|
||||
> skinné sur `SK_BodyA` / `SK_BodyB`.**
|
||||
|
||||
### Les couleurs
|
||||
|
||||
Chaque paramètre est posé sur **tous les éléments** de chaque mesh, sans chercher
|
||||
lequel le porte : un `SetVectorParameterValue` dont le paramètre n'existe pas est
|
||||
simplement ignoré. Le slot des yeux ignore donc `Skin`, celui de la peau ignore
|
||||
`Eye color`. Cela évite une table d'index de slots qui casserait au premier mesh
|
||||
mal rangé.
|
||||
|
||||
## 10. Réseau et persistance
|
||||
|
||||
```
|
||||
USurvivalUserSettings ──lit──► AnnounceCharacterAppearance()
|
||||
(.ini du joueur) │
|
||||
▼
|
||||
SubmitCharacterAppearance() ← POINT D'ENTRÉE UNIQUE
|
||||
│ │
|
||||
persiste Server_SetCharacterAppearance
|
||||
│
|
||||
ASurvivalPlayerState (autorité)
|
||||
│
|
||||
┌──────────┴──────────┐
|
||||
OnRep (clients) appel direct (hôte)
|
||||
└──────────┬──────────┘
|
||||
▼
|
||||
ApplyAppearanceToPawn()
|
||||
```
|
||||
|
||||
**`SubmitCharacterAppearance()` persiste ET annonce, jamais l'un sans l'autre.**
|
||||
Persister sans annoncer laisserait le joueur seul à se voir changé ; annoncer
|
||||
sans persister le ferait repartir au visage par défaut au lancement suivant.
|
||||
|
||||
L'apparence vit sur le **PlayerState** et pas sur le pawn : elle décrit le
|
||||
JOUEUR, pas le corps. Le pawn meurt et réapparaît, le PlayerState non —
|
||||
réapparaître avec le visage d'un autre n'aurait aucun sens, alors que perdre son
|
||||
inventaire au sol est la règle du jeu.
|
||||
|
||||
**Rien n'est validé côté PlayerState, délibérément** : borner un index demande le
|
||||
catalogue, que seul le composant connaît — or il passe déjà tout ce qu'il monte
|
||||
par `Catalog->Sanitize()`, sur chaque machine.
|
||||
|
||||
## 11. Le parcours de l'écran
|
||||
|
||||
```
|
||||
clic Jouer → fondu au noir → SetViewTarget(BP_CharacterPreview)
|
||||
→ colonne affichée → fondu depuis le noir
|
||||
├─ « Jouer » → apparence persistée → fondu de départ → GameScene
|
||||
└─ « Retour » → fondu → nouvel angle de menu
|
||||
```
|
||||
|
||||
On ne charge **aucune map** : le mannequin est posé dans `MenuScene`. La bascule
|
||||
de caméra se fait pendant que l'écran est noir — un travelling depuis l'angle du
|
||||
menu traverserait le décor.
|
||||
|
||||
**L'écran part toujours de l'apparence par défaut**, jamais de la persistée.
|
||||
C'est un provisoire assumé : à terme l'apparence appartiendra à la **sauvegarde**
|
||||
et non au joueur, et l'on regardera au clic sur *Jouer* si cette partie a déjà un
|
||||
personnage. `SetupCustomization(Preview, Appearance)` prend déjà l'apparence en
|
||||
paramètre — il n'y aura qu'à changer son origine.
|
||||
|
||||
Laisser **`Customization Menu Class` vide** sur `BP_MainMenuPlayerController`
|
||||
restaure l'ancien comportement (Jouer lance directement). Pratique pour tester
|
||||
autre chose sans traverser l'écran.
|
||||
|
||||
## 12. Les pièges du moteur rencontrés
|
||||
|
||||
Aucun n'était visible depuis le code. Ils sont ici pour ne pas être redécouverts.
|
||||
|
||||
### Les `UFUNCTION(Exec)` d'un `UActorComponent` ne sont JAMAIS appelées
|
||||
|
||||
La chaîne de routage (`UPlayer::Exec`, dans `Player.cpp`) interroge le monde, le
|
||||
`PlayerInput`, le contrôleur, le **pawn**, le HUD, le GameMode, le `CheatManager`,
|
||||
le GameState et le camera manager — **jamais les composants**, `AActor` ne
|
||||
surchargeant pas `ProcessConsoleExec` pour les parcourir. Et rien ne proteste :
|
||||
la commande est introuvable, sans message.
|
||||
|
||||
D'où `EmberPart` / `EmberColor` / `EmberAppearanceDump` posées sur `AFpsPlayer`,
|
||||
qui délègue. *(`EmberJoin` marche parce que `UGameInstance`, lui, route vers ses
|
||||
subsystems.)*
|
||||
|
||||
### `OverrideMaterials` survit à un changement de mesh
|
||||
|
||||
`SetSkeletalMeshAsset` remplace l'asset mais ne touche pas au tableau
|
||||
`OverrideMaterials`, qui appartient au **composant**. Sans vidage préalable,
|
||||
passer du corps A au corps B garde le MID dérivé de `MI_BodyA_a` : on obtient une
|
||||
silhouette féminine peinte avec la peau et les abdominaux du modèle masculin.
|
||||
Le symptôme trompe, parce que le mesh, lui, a bien changé.
|
||||
|
||||
D'où `SetPartMesh()`, qui appelle `EmptyOverrideMaterials()` avant d'assigner —
|
||||
et ne fait rien si le mesh est déjà le bon, pour que les MID survivent à un simple
|
||||
changement de couleur.
|
||||
|
||||
### L'ordre des slots de matériau varie d'un mesh à l'autre
|
||||
|
||||
Sur `SKM_HeadA_01`, l'**Element 0 est celui des YEUX** et l'Element 1 celui de la
|
||||
peau — l'inverse de l'intuition. Les six têtes viennent de deux packs différents,
|
||||
rien ne garantit qu'elles s'accordent.
|
||||
|
||||
On cherche donc le slot **par son nom** (`EyeMaterialSlotHint`, valeur `Eye`), et
|
||||
si aucun ne correspond on ne pose rien et on l'écrit dans le log : écraser le slot
|
||||
de la peau repeint le visage entier de la couleur des yeux, symptôme bien plus
|
||||
long à diagnostiquer qu'une ligne de log.
|
||||
|
||||
### Un nom de paramètre matériau se lit dans le Material INSTANCE
|
||||
|
||||
Le binaire de `M_Head_Base` contient la chaîne « Skin Base color », mais le
|
||||
paramètre réellement exposé s'appelle **`Skin`**. Le chercher sous l'autre nom
|
||||
dans `MI_HeadA_01_a` ne rend aucun résultat.
|
||||
|
||||
Corollaire : les noms de paramètres sont des `UPROPERTY` du catalogue et pas des
|
||||
constantes C++, pour qu'une erreur se corrige sans recompiler.
|
||||
|
||||
### `LeaderPoseComponent` est `BlueprintReadOnly`
|
||||
|
||||
Elle n'apparaît **jamais** dans le Details panel — la section « Leader Pose
|
||||
Component » qu'on y voit ne contient que des cases de bornes et de LOD. Elle ne
|
||||
s'assigne que par `SetLeaderPoseComponent()`, en C++ ou en Construction Script.
|
||||
|
||||
### `Approx Size` d'un Skeletal Mesh n'est pas sa hauteur visible
|
||||
|
||||
Les bounds incluent une marge et celles du Physics Asset. `SKM_BodyA` affiche 184
|
||||
alors que le personnage entier mesure à peu près autant. Pour juger une taille :
|
||||
viewport en vue **Front**, et la ligne verte (`Z = 0`) comme repère — le bas de la
|
||||
capsule est à `-HalfHeight`, le haut à `+HalfHeight`.
|
||||
|
||||
### `Initialize` masque une virtuelle d'`UUserWidget`
|
||||
|
||||
`UUserWidget::Initialize()` existe et est virtuelle. Une surcharge de signature
|
||||
différente la **masque** au lieu de la surcharger (C4263/C4264). D'où
|
||||
`SetupCustomization`. Même famille de piège que les membres nommés `Player` ou
|
||||
`Slot` dans une classe dérivée du moteur.
|
||||
|
||||
## 13. Les commandes console
|
||||
|
||||
Sur le pawn, en partie. Elles appliquent en local **et** soumettent (persistance
|
||||
+ annonce au serveur), donc elles testent toute la chaîne.
|
||||
|
||||
```
|
||||
EmberPart body 0..1 silhouette
|
||||
EmberPart head 0..5 visage
|
||||
EmberPart hair 0..12 coiffure
|
||||
EmberPart eyebrows 0..5 sourcils
|
||||
EmberPart beard 0..5 barbe
|
||||
EmberPart mustache 0..5 moustache
|
||||
EmberPart eyes 0..4 forme d'iris
|
||||
EmberPart beard -1 retire la pièce
|
||||
|
||||
EmberColor skin 0..11 teint
|
||||
EmberColor hair 0..5 cheveux + barbe + sourcils + moustache
|
||||
EmberColor eye 0..4 iris
|
||||
EmberColor underwear 0..3 sous-vêtement
|
||||
|
||||
EmberAppearanceDump état courant + nombre d'options par catégorie
|
||||
```
|
||||
|
||||
`EmberAppearanceDump` est le premier réflexe de diagnostic : un compte à **0**
|
||||
signale une liste vide dans le catalogue.
|
||||
|
||||
## 14. Dépannage
|
||||
|
||||
| Symptôme | Cause |
|
||||
|---|---|
|
||||
| La colonne de l'écran est vide | `Catalog`, `Category Widget Class` ou `Option Widget Class` non assignés dans les **Class Defaults** de `WBP_CharacterCustomization` (mode **Graph** → *Class Defaults*) |
|
||||
| *Jouer* lance la partie sans montrer l'écran | `Customization Menu Class` vide sur `BP_MainMenuPlayerController` |
|
||||
| Écran noir qui ne s'éclaircit pas | aucun `BP_CharacterPreview` dans la map — un avertissement l'écrit dans l'Output Log |
|
||||
| Le visage ne suit pas la couleur de peau | `Head Skin Color Parameter` faux dans le catalogue (`Skin`, pas `Skin Base color`) |
|
||||
| Le visage entier prend la couleur des yeux | `Eye Material Slot Hint` ne correspond à aucun slot — le log liste les slots disponibles |
|
||||
| Le corps B garde la peau du corps A | régression de `SetPartMesh()` : les overrides ne sont plus vidés |
|
||||
| Une pièce flotte détachée du corps | leader pose non posé sur ce composant |
|
||||
| Corps figé en pose de référence | `Anim Class` absente du Body Set |
|
||||
| Les pastilles de couleur sont invisibles | le pinceau d'`OptionImage` est en `Draw As = Image` sans texture — un pinceau en mode Image sans texture ne peint rien |
|
||||
| En PIE à 2, les deux joueurs sont identiques | normal : les deux fenêtres lisent le même `.ini`. Change l'un des deux à la console |
|
||||
|
||||
## 15. Limites connues
|
||||
|
||||
- **Les bras vus en 1re personne ne sont pas customisés.** Leur squelette
|
||||
(`BasePose_Skeleton`) n'a rien à voir avec `SK_BodyA` ; les rendre
|
||||
customisables imposerait de retargeter toutes les animations FPS. Raft assume
|
||||
le même compromis.
|
||||
- **La physique de cheveux est perdue en leader pose.** Coiffures, barbes et
|
||||
moustaches ont leur physics asset, mais l'AnimBP post-process d'un *follower*
|
||||
ne tourne pas. La démo du pack accepte déjà ce compromis.
|
||||
- **Tatouages et maquillage ne sont pas exposés.** `M_Head_Base` et
|
||||
`M_Body_Base` en ont pourtant les paramètres : ce ne sont que des couleurs et
|
||||
des masques de plus à ajouter au catalogue, aucune refonte.
|
||||
- **Pas d'animation accroupie.** Le pack n'en livre pas. Mais le squelette est
|
||||
celui du **Mannequin UE5** (`ik_foot_root`, `index_metacarpal_l`…), donc toute
|
||||
animation vendue « UE5 Mannequin compatible » se branche **sans retarget**.
|
||||
|
||||
## 16. Étendre
|
||||
|
||||
**Ajouter une catégorie** (tatouages, maquillage) :
|
||||
|
||||
1. un champ dans `FCharacterAppearance`
|
||||
2. une valeur dans `ECharacterPartCategory` (mesh) ou une palette (couleur)
|
||||
3. une valeur dans `ECustomizationCategory` + son libellé dans
|
||||
`CharacterCustomizationLabels::Get`
|
||||
4. les branches correspondantes dans `GetOptionCountForCategory`,
|
||||
`GetCurrentIndexForCategory` et `SetIndexForCategory`
|
||||
|
||||
L'écran, lui, n'a rien à changer : la colonne se construit depuis l'enum.
|
||||
|
||||
**Brancher les sauvegardes** : une seule ligne, dans
|
||||
`AMainMenuPlayerController::OpenCustomization()` — passer l'apparence de la save
|
||||
au lieu de `FCharacterAppearance()`.
|
||||
+452
@@ -0,0 +1,452 @@
|
||||
# Coffres
|
||||
|
||||
Contenants posés dans le monde : on les ouvre, on y range, on y reprend. Le couvercle se lève
|
||||
pour tout le monde, le contenu est partagé et accessible à plusieurs en même temps.
|
||||
|
||||
**Fichiers** : `StorageContainer.h/.cpp`, plus le panneau de coffre d'`InventoryScreenWidget`,
|
||||
l'API de transfert d'`InventoryComponent` et le bloc `UI|Storage` de `FpsPlayerController`.
|
||||
|
||||
---
|
||||
|
||||
## 1. Le principe
|
||||
|
||||
**Un coffre n'a aucun modèle de données à lui.** Son contenu est un `UInventoryComponent`,
|
||||
exactement le même que celui du joueur, simplement configuré sans barre rapide. Tout le reste
|
||||
en découle :
|
||||
|
||||
- le glisser-déposer entre le sac et le coffre ne convertit rien — ce sont les mêmes cases,
|
||||
le même `FInventorySlot` ;
|
||||
- la durabilité d'une hache traverse le coffre **sans une ligne de code**, puisque l'usure
|
||||
vit sur le slot ;
|
||||
- il n'y a pas de « format coffre » à faire évoluer le jour où l'inventaire change.
|
||||
|
||||
**Le coffre ne décide de rien.** Il se contente d'appeler `OpenStorage(this)` sur le
|
||||
controller. C'est ce dernier qui referme le couvercle — par Échap, par la mort du joueur, ou
|
||||
parce qu'il s'est éloigné. Un coffre qui déciderait seul finirait ouvert dans la moitié des
|
||||
cas, et il n'a de toute façon aucun moyen de savoir que l'écran s'est fermé.
|
||||
|
||||
**Ce n'est pas un écran à part**, c'est un panneau de plus sur `WBP_InventoryScreen` — voir §5.
|
||||
|
||||
---
|
||||
|
||||
## 2. L'acteur `AStorageContainer`
|
||||
|
||||
### 2.1 Composants
|
||||
|
||||
| Composant | Rôle |
|
||||
|---|---|
|
||||
| `BaseMesh` | La caisse : `SM_Chest_Bottom`. Racine, **purement visuelle**, collision désactivée |
|
||||
| `InteractionBox` | L'**unique** collision du coffre. Fille de la caisse |
|
||||
| `LidMesh` | Le couvercle : `SM_Chest_Top`. Enfant direct de la caisse, collision désactivée |
|
||||
| `Storage` | Le contenu. Un `UInventoryComponent` ordinaire |
|
||||
|
||||
### 2.2 Réglages exposés
|
||||
|
||||
**Catégorie `Storage`**
|
||||
|
||||
| Propriété | Défaut | Effet |
|
||||
|---|---|---|
|
||||
| `SlotCount` | 20 | Nombre de cases. **Réglable par instance** : le même Blueprint donne un petit coffre de 12 et un grand de 30 |
|
||||
| `ColumnCount` | 6 | Cases par ligne dans l'écran. Purement visuel |
|
||||
| `DisplayName` | `Chest` | Titre affiché au-dessus de la grille |
|
||||
| `InteractionPrompt` | `Ouvrir le coffre` | Texte affiché quand le joueur vise |
|
||||
| `StartingItems` | — | Contenu versé **une seule fois** au `BeginPlay` |
|
||||
|
||||
`StartingItems` est de la donnée de level design, pas du gameplay : un coffre de départ, une
|
||||
cache à trouver. Ce qui ne rentre pas part dans l'Output Log en warning plutôt que de
|
||||
disparaître en silence.
|
||||
|
||||
**Catégorie `Storage|Collision`**
|
||||
|
||||
| Propriété | Défaut | Effet |
|
||||
|---|---|---|
|
||||
| `bAutoFitInteractionBox` | ☑ | La boîte épouse seule les deux meshes, couvercle fermé |
|
||||
| `InteractionBoxPadding` | 0 cm | Marge **additive** autour du volume trouvé |
|
||||
|
||||
**Catégorie `Storage|Lid`**
|
||||
|
||||
| Propriété | Défaut | Effet |
|
||||
|---|---|---|
|
||||
| `OpenRotation` | `(-100, 0, 0)` | Rotation **relative** du couvercle ouvert. Le repos vaut toujours zéro |
|
||||
| `LidDuration` | 0.35 s | Durée de l'ouverture |
|
||||
|
||||
**Catégorie `Storage|Sound`** — `OpenSound`, `CloseSound`. Facultatifs : un champ vide ne joue
|
||||
rien.
|
||||
|
||||
### 2.3 API publique
|
||||
|
||||
```cpp
|
||||
void SetOpen(bool bInOpen); // SERVEUR uniquement. Lève ou rabat le couvercle
|
||||
bool IsOpen() const;
|
||||
UInventoryComponent* GetStorage() const;
|
||||
FText GetDisplayName() const;
|
||||
int32 GetColumnCount() const;
|
||||
```
|
||||
|
||||
`SetOpen` est appelée par le **controller**, pas par le coffre : lui seul sait quand l'écran
|
||||
se referme.
|
||||
|
||||
---
|
||||
|
||||
## 3. Le couvercle
|
||||
|
||||
### 3.1 La rotation se fait sur le pivot du mesh
|
||||
|
||||
**C'est `LidMesh` lui-même qu'on tourne**, et un `UStaticMeshComponent` pivote toujours autour
|
||||
de l'origine de son mesh. La charnière est donc celle que l'artiste a donnée à `SM_Chest_Top` —
|
||||
rien à placer, rien à régler.
|
||||
|
||||
> Un `SceneComponent` vide `LidPivot` a existé entre la caisse et le couvercle, pour découpler
|
||||
> la charnière du pivot du pack. Il a été **retiré** : il fallait le placer à la main dans
|
||||
> chaque Blueprint, et laissé à `(0,0,0)` il faisait tourner le couvercle autour du centre de
|
||||
> la caisse — c'est-à-dire à travers elle. C'était le symptôme, pas une configuration exotique.
|
||||
|
||||
Corollaire assumé : si un pack livre un couvercle dont le pivot est au centre, **on corrige
|
||||
l'asset, pas le code** — Modeling Mode > XForm > **Edit Pivot** sur le Static Mesh, pivot posé
|
||||
sur l'arête arrière, Accept. C'est payé une fois par pack de coffre.
|
||||
|
||||
`OpenRotation` est un `FRotator` complet et pas un simple angle : selon l'orientation dans
|
||||
laquelle le mesh a été exporté, la charnière peut tomber sur le **pitch**, le **yaw** ou le
|
||||
**roll**. On règle les trois à l'œil dans le viewport plutôt que d'aller comprendre quel axe
|
||||
local du mesh pointe où.
|
||||
|
||||
### 3.2 L'animation
|
||||
|
||||
Un `Tick`, mais **éteint au repos** (`bStartWithTickEnabled = false`). `ApplyOpenState()` le
|
||||
rallume, et le tick s'éteint lui-même en fin de course.
|
||||
|
||||
```cpp
|
||||
LidAlpha = FMath::FInterpConstantTo(LidAlpha, Target, DeltaTime, 1.f / LidDuration);
|
||||
const float Eased = FMath::SmoothStep(0.f, 1.f, LidAlpha);
|
||||
LidMesh->SetRelativeRotation(OpenRotation * Eased);
|
||||
```
|
||||
|
||||
- **`SmoothStep`** : un couvercle à vitesse constante démarre et s'arrête d'un coup, ce qui se
|
||||
lit comme une saccade sur un objet lourd.
|
||||
- **L'alpha repart de sa valeur courante**, donc refermer un coffre à moitié ouvert ne saute
|
||||
pas.
|
||||
- **La valeur exacte est forcée avant de se rendormir**. Sans ça le couvercle resterait à un
|
||||
poil de sa position finale pour le reste de la partie.
|
||||
- **`BeginPlay` repose l'alpha à zéro** quelle que soit la rotation laissée dans le Blueprint :
|
||||
le repos *est* la référence de l'animation. Une rotation authorée sur `LidMesh` serait donc
|
||||
perdue au lancement.
|
||||
|
||||
---
|
||||
|
||||
## 4. La collision — une seule boîte
|
||||
|
||||
**Les deux 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 tourne** :
|
||||
|
||||
- compter sur les collisions des meshes 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 —
|
||||
viser le haut d'un coffre ouvert ne toucherait plus rien.
|
||||
|
||||
La boîte est fille de la **caisse**, jamais du couvercle : le coffre se vise exactement au même
|
||||
endroit ouvert et fermé. Sinon le prompt disparaîtrait au moment précis où l'on s'en sert.
|
||||
|
||||
Son profil est `BlockAll`, posé explicitement : elle a deux rôles indissociables, empêcher le
|
||||
joueur de traverser le coffre **et** bloquer le canal `Visibility` pour que le trace
|
||||
d'interaction la trouve. Un profil qui laisserait passer `Visibility` rendrait le coffre
|
||||
inutilisable sans qu'aucune erreur ne le signale.
|
||||
|
||||
### `FitInteractionBox()` — deux pièges
|
||||
|
||||
Appelée depuis `OnConstruction`, donc elle tourne **aussi dans l'éditeur** : changer de variante
|
||||
de coffre recale la boîte à l'instant, sans lancer le jeu.
|
||||
|
||||
1. **Lire les bounds de l'ASSET (`UStaticMesh::GetBoundingBox()`), pas `Mesh->Bounds`** — ces
|
||||
dernières sont déjà multipliées par l'échelle, que `SetBoxExtent` remultiplie.
|
||||
2. **Composer avec la transform RELATIVE à la racine**, pas la transform monde : la boîte doit
|
||||
rester juste où que l'acteur soit posé dans le niveau.
|
||||
|
||||
Même paire de pièges que dans `APickupItem::FitInteractionBox`.
|
||||
|
||||
---
|
||||
|
||||
## 5. L'écran
|
||||
|
||||
### 5.1 Un panneau, pas un écran
|
||||
|
||||
Le contenu du coffre s'affiche **à côté** de la grille du joueur, sur `WBP_InventoryScreen`,
|
||||
comme **frère du WidgetSwitcher** et `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 ». Un écran distinct le ferait sauter à chaque fois.
|
||||
|
||||
Ce n'est **pas non plus un troisième onglet** : les deux grilles doivent être visibles
|
||||
*ensemble*, ce qu'un WidgetSwitcher interdit par construction.
|
||||
|
||||
### 5.2 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, et UMG refuse de le compiler. D'où deux conséquences directes :
|
||||
|
||||
- **`bAutoBindToPawn` doit être DÉCOCHÉ** sur la grille de coffre. Sans ça elle s'accroche au
|
||||
pawn dans son `NativeConstruct` — qui s'exécute avant que l'écran ait eu le temps de lui
|
||||
donner le conteneur — et le sac du joueur s'affiche des **deux** côtés.
|
||||
- **Le panneau de détail est coupé par le code, jamais par une case à cocher**
|
||||
(`SetItemDetailsEnabled`). Un réglage par instance serait un bug qui attend qu'on oublie de
|
||||
le décocher sur l'une des deux.
|
||||
|
||||
### 5.3 Ce que l'écran fait à l'ouverture
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| `StorageGrid->SetColumnCount` | La forme vient du **coffre posé dans le niveau**, pas du Widget Blueprint |
|
||||
| `StorageGrid->BindToInventory` | Branche la grille sur le contenu |
|
||||
| Cibles de clic droit croisées | Sac → coffre et coffre → sac (§6.2) |
|
||||
| `InventoryPage->SetItemDetailsEnabled(false)` | Le sac perd son panneau de détail : trois colonnes seraient illisibles |
|
||||
| `ShowInventoryTab()` | Un coffre ouvert sous l'onglet Fabrication n'aurait aucun sens |
|
||||
|
||||
Et **l'onglet Fabrication est grisé** tant qu'un coffre est ouvert : le panneau étant frère du
|
||||
WidgetSwitcher, il resterait affiché à côté de la grille de recettes. La touche de craft, elle,
|
||||
**rend le coffre** plutôt que d'ignorer l'appui — la touche exprime une intention claire.
|
||||
|
||||
`HideStorage()` est appelée dès `NativeConstruct` : l'état par défaut est **reposé** à chaque
|
||||
construction plutôt qu'hérité de la Designer ou de la session précédente. Elle coupe aussi les
|
||||
transferts rapides **avant** de débrancher — une case qui garderait le coffre en mémoire y
|
||||
enverrait encore des objets au prochain clic droit, panneau fermé.
|
||||
|
||||
Tous les éléments de coffre sont en **`BindWidgetOptional`** : l'écran doit rester valide dans
|
||||
un projet où le panneau n'a pas encore été construit, sinon ajouter les coffres casserait la
|
||||
compilation de `WBP_InventoryScreen` — et donc l'inventaire entier.
|
||||
|
||||
---
|
||||
|
||||
## 6. Les transferts
|
||||
|
||||
### 6.1 Un seul déplacement pour tout
|
||||
|
||||
```cpp
|
||||
static bool TransferSlot(UInventoryComponent* From, int32 FromIndex,
|
||||
UInventoryComponent* To, int32 ToIndex, int32 Quantity);
|
||||
```
|
||||
|
||||
**Statique**, parce qu'elle n'appartient ni à la source ni à la destination : elle arbitre entre
|
||||
les deux. `MoveItem()` et `MoveItemQuantity()` ne sont plus que des appels avec `From == To`.
|
||||
|
||||
Elle diffuse `OnInventoryChanged` **des deux côtés**. Oublier la seconde diffusion laisse la
|
||||
grille d'en face afficher un état périmé jusqu'au prochain événement, ce qui se lit comme un
|
||||
objet qui disparaît.
|
||||
|
||||
### 6.2 Clic droit = envoyer en face
|
||||
|
||||
`QuickTransferSlot()` passe par `AddItem()`, donc **exactement comme un ramassage** : piles
|
||||
entamées d'abord, puis barre rapide, puis grille. Ce qui ne rentre pas reste sur place.
|
||||
|
||||
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.
|
||||
La **barre rapide** reçoit elle aussi le coffre, pour que le clic droit y marche par-dessus
|
||||
l'écran.
|
||||
|
||||
### 6.3 « Tout ranger » / « Tout prendre »
|
||||
|
||||
```cpp
|
||||
int32 TransferAllTo(UInventoryComponent* To, int32 FirstIndex = 0);
|
||||
```
|
||||
|
||||
- **Tout ranger** part de `GetBackpackStartIndex()` : la barre rapide porte les outils qu'on
|
||||
vient d'utiliser, les ranger d'office serait une punition déguisée en confort.
|
||||
- **Tout prendre** part de l'index 0 — un coffre n'a pas de barre rapide. Ce qui ne rentre pas
|
||||
dans le sac reste dans le coffre.
|
||||
|
||||
---
|
||||
|
||||
## 7. Le cycle d'ouverture
|
||||
|
||||
Le controller est le seul maître. `ReleaseActiveStorage()` est le **point de sortie unique** :
|
||||
toute fermeture d'écran y passe, quelle qu'en soit la cause.
|
||||
|
||||
| Déclencheur | Chemin |
|
||||
|---|---|
|
||||
| Interagir avec le coffre | `Interact_Implementation` → `OpenStorage()` |
|
||||
| Échap | `HandlePauseInput()` — le coffre passe avant l'inventaire |
|
||||
| Tab | `SetInventoryScreenOpen(false)` |
|
||||
| Touche de craft | Rend le coffre, puis affiche l'onglet Fabrication |
|
||||
| Mort du joueur | Fermeture de l'écran |
|
||||
| Éloignement | `CheckStorageDistance()`, timer à 0,25 s |
|
||||
| Coffre détruit | Idem, la `TWeakObjectPtr` est nulle |
|
||||
| Ouvrir un autre coffre | `OpenStorage()` rend le précédent avant |
|
||||
|
||||
**`MaxStorageDistance` (400 cm)** : sans elle on viderait un coffre depuis l'autre bout de la
|
||||
carte, rien n'obligeant le joueur à rester devant une fois l'écran ouvert. Surveillée par un
|
||||
**timer**, jamais un Tick, et **seulement pendant que l'écran est ouvert** — un timer permanent
|
||||
pour une condition vraie deux minutes par partie serait du gaspillage. `DistSquared`, pour ne
|
||||
pas prendre une racine carrée quatre fois par seconde.
|
||||
|
||||
Le timer est aussi nettoyé à la destruction du HUD : il rappellerait une méthode d'un controller
|
||||
en train de disparaître.
|
||||
|
||||
---
|
||||
|
||||
## 8. Réseau
|
||||
|
||||
**Autorité serveur sur tout**, comme pour le reste du monde.
|
||||
|
||||
| Élément | Réplication |
|
||||
|---|---|
|
||||
| `bOpen` | `ReplicatedUsing = OnRep_Open` — voir le couvercle d'un coffre se lever est le seul signe qu'un coéquipier fouille dedans |
|
||||
| `Slots` du conteneur | Répliqué à **tout le monde** par `UInventoryComponent` |
|
||||
| L'animation, le son | **Rien** : dérivés de `bOpen` des deux côtés par `ApplyOpenState()` |
|
||||
|
||||
### Les quatre pièges
|
||||
|
||||
1. **`Interact_Implementation` s'exécute sur le SERVEUR.** `PlayerController->OpenStorage(...)`
|
||||
y créerait un widget dans le vide. D'où `Client_OpenStorage` : le serveur lève le couvercle
|
||||
(état du monde) et **renvoie l'ordre d'écran** au client. C'est le piège le plus
|
||||
contre-intuitif du portage, parce que le code *semble* correct et marche parfaitement chez
|
||||
l'hôte.
|
||||
2. **`SetOpen` ne fait rien sans autorité.** Chez un client, refermer l'écran doit passer par
|
||||
`Server_ReleaseStorage` — sinon le coffre resterait ouvert pour les trois autres joueurs
|
||||
alors que l'écran est fermé ici.
|
||||
3. **`FindNetProxy` est le passage obligé des transferts.** Un `UInventoryComponent` de coffre
|
||||
n'appartient à aucune connexion : une RPC émise depuis lui est **silencieusement jetée**. On
|
||||
route donc toujours par l'inventaire du pawn local, et la source comme la destination sont
|
||||
passées **en paramètres** — jamais déduites du récepteur, sinon « Tout prendre » viderait le
|
||||
sac du joueur dans lui-même.
|
||||
4. **`Slots` est répliqué à tout le monde, pas au seul propriétaire.** Ce n'est pas un
|
||||
raccourci : les conditions de `GetLifetimeReplicatedProps` sont mises en cache **par classe**,
|
||||
pas par instance. Un `COND_OwnerOnly` frapperait donc aussi le composant du coffre — or un
|
||||
coffre n'appartient à personne, et sa grille n'arriverait chez aucun joueur.
|
||||
|
||||
**Accès simultané, pas de verrou.** Deux joueurs peuvent fouiller le même coffre : `TransferSlot`
|
||||
diffuse déjà `OnInventoryChanged` des deux côtés, et tous les widgets y sont abonnés. L'UI se
|
||||
recâble donc toute seule à chaque `OnRep_Slots()`.
|
||||
|
||||
**Pas de prédiction.** Le client envoie sa RPC et attend la réplication. Quelques dizaines de
|
||||
millisecondes sur un relais Steam, invisibles pour un glisser-déposer, alors qu'une prédiction
|
||||
fausse donnerait une pile qui saute.
|
||||
|
||||
---
|
||||
|
||||
## 9. Guide éditeur
|
||||
|
||||
### 9.1 Le Blueprint du coffre
|
||||
|
||||
1. Content Browser → `Content/Game/Building/Storage/` → clic droit → **Blueprint Class** →
|
||||
**All Classes** → `StorageContainer` → nomme-le **`BP_Chest_Wooden`**.
|
||||
2. Ouvre-le. Sélectionne **`BaseMesh`** → Details → **Static Mesh** = `SM_Chest_Bottom`.
|
||||
3. Sélectionne **`LidMesh`** → **Static Mesh** = `SM_Chest_Top`.
|
||||
4. Toujours sur `LidMesh`, Details → **Transform** :
|
||||
- **Location** : les deux meshes viennent du même export, donc **`0, 0, 0`** les repose l'un
|
||||
sur l'autre. Ajuste seulement s'il flotte ou s'enfonce.
|
||||
- **Rotation** : **`0, 0, 0`** obligatoirement. `BeginPlay` écrase cette valeur — une
|
||||
rotation laissée ici serait perdue au lancement.
|
||||
5. Sélectionne l'acteur racine (`Self`) → Details → **Storage | Lid** → **`Open Rotation`**.
|
||||
Le défaut est `Pitch = -100`. Tape des valeurs et regarde le viewport : si le couvercle
|
||||
bascule sur le côté, essaie le **Roll**, puis le **Yaw**.
|
||||
6. **`Storage`** → règle `Slot Count` et `Column Count`. Mets un `Display Name` (« Coffre en
|
||||
bois ») et un `Interaction Prompt`.
|
||||
7. **`Storage | Sound`** → `Open Sound` / `Close Sound` si tu en as.
|
||||
8. Laisse **`Auto Fit Interaction Box`** coché : la boîte se cale seule sur les deux meshes.
|
||||
Vérifie-la avec **Show > Collision** dans le viewport.
|
||||
9. **Compile** et **Save**.
|
||||
|
||||
Pose-le dans le niveau. `Slot Count`, `Column Count`, `Display Name` et `Starting Items` se
|
||||
règlent **par instance** dans le World Outliner : un seul Blueprint sert tous les coffres.
|
||||
|
||||
### 9.2 Le panneau de coffre dans `WBP_InventoryScreen`
|
||||
|
||||
À ne faire qu'une fois. Tout est en `BindWidgetOptional`, donc l'écran compile même à mi-chemin.
|
||||
|
||||
1. Ouvre **`WBP_InventoryScreen`**.
|
||||
2. Dans la Designer, à côté du `TabSwitcher` — **frère, pas enfant** — pose un **Horizontal Box**
|
||||
ou une **Border**, et renomme-le **`StoragePanel`**. Mets sa **Visibility** à `Collapsed`
|
||||
(le code la repose de toute façon).
|
||||
3. Dedans, un **Vertical Box** contenant :
|
||||
- un **Text Block** nommé **`StorageTitle`** ;
|
||||
- ton **`WBP_Inventory`** (Palette → User Created), renommé **`StorageGrid`** ;
|
||||
- un **Horizontal Box** avec deux **Button** nommés **`StoreAllButton`** et
|
||||
**`TakeAllButton`**, chacun avec son Text Block (« Tout ranger », « Tout prendre »).
|
||||
4. **⚠ L'étape qu'on rate** : sélectionne **`StorageGrid`** → Details → **Inventaire** →
|
||||
**DÉCOCHE `Auto Bind To Pawn`**.
|
||||
5. Rappel : un **Canvas Panel a une taille désirée de zéro**. Si `StoragePanel` disparaît dans
|
||||
un conteneur en `Auto`, c'est ça — et un enfant en `Size = Fill` ne compte pas non plus dans
|
||||
la taille désirée de son parent.
|
||||
6. **Compile** et **Save**.
|
||||
|
||||
### 9.3 Le controller
|
||||
|
||||
Un seul réglage : `BP_FpsPlayerController` → Details → **UI | Storage** → **`Max Storage
|
||||
Distance`** (400 cm par défaut). Rien d'autre à brancher, le coffre appelle `OpenStorage`
|
||||
lui-même.
|
||||
|
||||
### 9.4 Vérifier que ça marche
|
||||
|
||||
1. Vise le coffre : le prompt affiche ton `Interaction Prompt`, et **au même endroit** que le
|
||||
coffre soit ouvert ou fermé.
|
||||
2. Interagis : le couvercle se lève en ~0,35 s, avec un départ et un arrêt adoucis, et l'écran
|
||||
d'inventaire s'ouvre avec **le sac inchangé** et le coffre à côté.
|
||||
3. L'onglet **Fabrication est grisé**, et le panneau de détail du sac a disparu.
|
||||
4. Glisse un objet du sac vers le coffre : **Maj** = la moitié, **Ctrl** = un seul.
|
||||
5. **Clic droit** sur une case : elle part en face. Fais-le depuis la barre rapide aussi.
|
||||
6. **Tout ranger** vide la grille mais **laisse la barre rapide** intacte.
|
||||
7. Recule de plus de 4 m : l'écran se ferme seul et le couvercle se rabat.
|
||||
8. Appuie sur la touche de craft coffre ouvert : le coffre est rendu et l'onglet Fabrication
|
||||
s'affiche.
|
||||
9. **En coop** (build Steam) : l'autre joueur voit le couvercle se lever, et un objet déposé
|
||||
apparaît chez lui sans qu'il ait à refermer et rouvrir.
|
||||
|
||||
---
|
||||
|
||||
## 10. Dépannage
|
||||
|
||||
| Symptôme | Cause |
|
||||
|---|---|
|
||||
| Le couvercle traverse la caisse / tourne de travers | Mauvais axe d'`Open Rotation`, ou pivot de `SM_Chest_Top` posé au centre du mesh — corrige-le dans Modeling Mode > XForm > Edit Pivot |
|
||||
| Le couvercle est décalé de la caisse | `Location` de `LidMesh` (§9.1 étape 4) |
|
||||
| Le couvercle saute au lancement | Une `Rotation` a été laissée sur `LidMesh` : `BeginPlay` la remet à zéro |
|
||||
| Le coffre n'est pas visable | `Auto Fit Interaction Box` décoché avec une boîte restée minuscule, ou profil de collision changé |
|
||||
| Le prompt disparaît une fois ouvert | La boîte a été reparentée sous `LidMesh` — elle doit rester fille de `BaseMesh` |
|
||||
| Le sac du joueur s'affiche **des deux côtés** | `Auto Bind To Pawn` resté coché sur `StorageGrid` |
|
||||
| Le panneau ne s'affiche pas du tout | `StoragePanel` ou `StorageGrid` mal nommé — **l'Output Log le dit** |
|
||||
| Le panneau est là mais vide et plat | Canvas Panel dans un conteneur `Auto`, ou tous les enfants en `Fill` |
|
||||
| Le coffre s'ouvre chez l'hôte, pas chez le client | `Client_OpenStorage` court-circuité, ou `Interact` appelé hors de `Execute_Interact` |
|
||||
| Le couvercle reste levé chez les autres après fermeture | Chemin de sortie qui ne passe pas par `ReleaseActiveStorage()` |
|
||||
| Un objet réapparaît après un transfert | Transfert exécuté côté client sans RPC — c'est la réplication qui le remet |
|
||||
| « Tout prendre » vide le sac dans lui-même | Source déduite du récepteur au lieu d'être passée en paramètre |
|
||||
| Les objets de départ manquent | `Slot Count` trop petit : le débordement part en warning dans l'Output Log |
|
||||
|
||||
---
|
||||
|
||||
## 11. Étendre
|
||||
|
||||
Gratuit, sans une ligne de C++ :
|
||||
|
||||
- **D'autres coffres** — le Blueprint est générique, seuls les meshes changent. La boîte
|
||||
d'interaction se recale seule.
|
||||
- **Des tailles différentes** — `Slot Count` par instance.
|
||||
- **Un coffre-piège, une cache** — `Starting Items`.
|
||||
|
||||
Ce qui demanderait du code :
|
||||
|
||||
- **La sauvegarde du contenu.** C'est le chantier réel : `Slots` n'est pas sérialisé
|
||||
aujourd'hui. Le format est déjà le bon — le même `FInventorySlot` que le sac, donc une seule
|
||||
sérialisation servira les deux.
|
||||
- **Les ressources du coffre comptant dans les recettes** — c'est la raison pour laquelle
|
||||
l'onglet Fabrication est grisé plutôt que masqué : le jour où `UCraftingComponent` saura lire
|
||||
un inventaire voisin, il suffira de le dégriser.
|
||||
- **Un cadenas, un propriétaire** — il n'y a aujourd'hui aucun verrou, et c'est délibéré : coop
|
||||
entre amis.
|
||||
- **Un coffre en un seul mesh** (tonneau, sac) — la `UBoxComponent` reste valable, il suffit de
|
||||
laisser `LidMesh` vide et `OpenRotation` à zéro. `FitInteractionBox` ignore déjà un mesh
|
||||
absent.
|
||||
|
||||
---
|
||||
|
||||
## 12. Ce que ce système a changé ailleurs
|
||||
|
||||
- **`UInventoryComponent::TransferSlot`** est devenue l'**unique** implémentation du déplacement.
|
||||
`MoveItem` / `MoveItemQuantity` ne sont plus que des appels avec `From == To`.
|
||||
- **`ConfigureAsContainer()`** : zéro barre rapide, donc `GetBackpackStartIndex()` vaut 0 et la
|
||||
grille affiche tout. Aucun nouveau modèle de données.
|
||||
- **`UInventoryWidget` a perdu son lien vers sa grille parente** : une case diffuse
|
||||
`OnHoverChanged` au lieu de connaître son propriétaire. Avec deux grilles à l'écran, l'index
|
||||
seul ne désignait plus rien de façon unique — d'où `GetHoveredSlot(inventaire, index)`.
|
||||
- **`NativeOnMouseLeave` n'arrive jamais quand un widget s'efface sous le curseur**, ce qui 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.
|
||||
- **`SetItemDetailsEnabled`** sur la grille, piloté par l'écran et non par une case à cocher.
|
||||
Reference in New Issue
Block a user