406 lines
18 KiB
Markdown
406 lines
18 KiB
Markdown
# 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()`.
|